Spring Boot + AWS S3: Upload and Download Files

Connecting a Spring Boot application to Amazon S3 is one of the most common real-world backend requirements. Whether you want to store images, documents, logs, or app data, S3 provides secure, scalable, and durable object storage.

In this tutorial, you’ll learn how to integrate Spring Boot with AWS S3 using AWS SDK v2 to build clean and reliable upload/download APIs.

High-Level Architecture

The overall workflow is simple:

  • Your Spring Boot application runs locally or on a server.
  • The app uses AWS SDK v2 to create an S3Client.
  • The S3Client connects to your S3 bucket using AWS credentials.
  • REST APIs handle upload and download requests and interact with S3 internally.

This setup works for local development, on-prem servers, and cloud deployments.

Step 1: Create a Spring Boot Project

Create a new project using Spring Initializr:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.x+
  • Dependencies: Spring Web
  • Java Version: 17+

Import the project into IntelliJ or your preferred IDE and verify the application starts successfully.

Step 2: Add AWS SDK v2 for S3

Add the following dependency in your pom.xml to communicate with S3:

<dependency>
  <groupId>software.amazon.awssdk</groupId>
  <artifactId>s3</artifactId>
  <version>LATEST_VERSION</version>
</dependency>

Reload Maven so the dependency downloads.

Step 3: Configure the S3Client Bean

Create a config package and add a class like S3Config. Inside it, define a bean that returns an S3Client.

package com.example.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class S3Config {

    @Value("${aws.region}")
    private String region;

    @Value("${aws.accessKey}")
    private String accessKey;

    @Value("${aws.secretKey}")
    private String secretKey;

    @Bean
    public S3Client s3Client() {
        AwsBasicCredentials creds = AwsBasicCredentials.create(accessKey, secretKey);

        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(creds))
                .build();
    }
}

The bean configuration typically loads:

  • AWS region
  • Access key
  • Secret key
  • Bucket name (configured separately)

Make sure when creating credentials the order is correct: access key first, secret key second. Reversing them causes errors like: “AWS Access Key ID you provided does not exist in our records.”

Step 4: Service Layer for Upload & Download

Create a service class (for example, S3Service) to encapsulate S3 operations.

Two essential methods:

1. Upload File

Use:

  • s3Client.putObject() for upload
  • PutObjectRequest for bucket, key, and metadata
  • RequestBody.fromBytes(file.getBytes()) to send file content

2. Download File

Use:

  • s3Client.getObjectAsBytes() to retrieve file data
  • Return the byte[] to the controller
package com.example.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Service
public class S3Service {

    private final S3Client s3Client;

    @Value("${aws.s3.bucketName}")
    private String bucketName;

    public S3Service(S3Client s3Client) {
        this.s3Client = s3Client;
    }

    // Upload
    public String uploadFile(MultipartFile file) throws Exception {
        String key = file.getOriginalFilename();

        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();

        s3Client.putObject(
                putObjectRequest,
                RequestBody.fromBytes(file.getBytes())
        );

        return "File uploaded successfully: " + key;
    }

    // Download
    public byte[] downloadFile(String key) {
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .build();

        ResponseBytes objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
        return objectBytes.asByteArray();
    }
}

This approach keeps AWS-related logic in a single place.

Step 5: REST Controller for File APIs

Create S3Controller with endpoints:

POST /upload

  • Accepts MultipartFile
  • Calls s3Service.uploadFile()
  • Returns a success message

GET /download

  • Accepts key or fileName as a request parameter
  • Calls s3Service.downloadFile()
  • Returns byte[] with the file content
package com.example.controller;

import com.example.service.S3Service;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class S3Controller {

    private final S3Service s3Service;

    public S3Controller(S3Service s3Service) {
        this.s3Service = s3Service;
    }

    // Upload API
    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
        return s3Service.uploadFile(file);
    }

    // Download API
    @GetMapping("/download")
    public ResponseEntity downloadFile(@RequestParam String fileName) {
        byte[] data = s3Service.downloadFile(fileName);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName)
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(data);
    }
}

You can test both endpoints using Postman.

Step 6: Create IAM User & Access Keys

Create a dedicated IAM user for your Spring Boot app:

  • Go to IAM → Users → Create user
  • Assign S3 permissions (e.g., S3FullAccess)
  • Generate an access key (use case: “Local code”)
  • Copy Access Key ID and Secret Key safely

These credentials allow your local application to access S3.

Step 7: Configure Application Properties

In application.properties or application.yml, configure:

aws.region=ap-south-1
aws.accessKey=YOUR_ACCESS_KEY
aws.secretKey=YOUR_SECRET_KEY
aws.s3.bucketName=your-bucket-name

Ensure the bucket name matches exactly — no extra spaces, newlines, or quotes. Incorrect values cause errors like: “The specified bucket is not valid.”

Step 8: Test Upload & Download

Upload Test (POST /upload)

In Postman:

  • Create a POST request to http://localhost:8080/upload
  • Form-data → key: file → type: File
  • Select a sample text/image file

You should see a success message. Verify the file in your S3 bucket.

Download Test (GET /download)

  • GET request to http://localhost:8080/download?fileName=sample-demo.txt
  • You’ll receive the file bytes in the response

Common Errors & Fixes

“The specified bucket is not valid”

  • Check bucket name formatting
  • Ensure it matches the region

“AWS Access Key ID you provided does not exist”

  • Check access key + secret key order
  • Verify the correct IAM user

Security Best Practices

  • Never hardcode credentials in source code
  • Do not commit access keys to Git
  • Prefer environment variables for credentials
  • Use IAM roles in production (EC2, ECS, Lambda)

Final Thoughts

By combining Spring Boot, AWS S3, and AWS SDK v2, you can build powerful file upload and download features with minimal code. With the right S3Client configuration, clean service layer, and simple REST APIs, your application becomes production-ready for real-world storage scenarios.

This tutorial sets the foundation for more advanced topics like presigned URLs, multipart uploads, and integrating S3 with CloudFront or Lambda.

Comments

Popular posts from this blog

Spring Boot on AWS EC2: Upload to S3 Securely Using IAM Role

Java Streams Intermediate Operations Explained with Examples

ConcurrentHashMap vs Synchronized HashMap in Java