Full Stack • Java • System Design • Cloud • AI Engineering

Spring Boot with Amazon S3

Learn how to integrate Spring Boot with Amazon S3 step by step using AWS SDK for Java, file upload, file download, file delete, IAM permissions, presigned URLs, input/output examples, and production best practices.


Introduction

Amazon S3 is one of the most commonly used AWS services for storing files, images, videos, documents, backups, logs, and static assets.

In real-time Spring Boot applications, S3 is commonly used for:

  • User profile images
  • PDF documents
  • Excel uploads
  • Reports
  • Application logs
  • Backups
  • Static files
  • Data lake storage

In this article, we will build a Spring Boot application that integrates with Amazon S3 using the AWS SDK for Java 2.x.

We will implement:

  • Upload file to S3
  • Download file from S3
  • Delete file from S3
  • List files from S3
  • Generate presigned URL
  • Configure IAM permissions
  • Follow production best practices

What You Will Learn

  • What is Amazon S3?
  • Why use S3 with Spring Boot?
  • How to create an S3 bucket
  • How to configure AWS credentials
  • How to add AWS SDK dependency
  • How to create S3 client
  • How to upload files
  • How to download files
  • How to delete files
  • How to list files
  • How to generate presigned URLs
  • Common errors and fixes
  • Security best practices

What is Amazon S3?

Amazon S3 stands for Simple Storage Service.

It is an object storage service used to store and retrieve any amount of data.

S3 stores data as objects inside buckets.

Bucket = Container
Object = File
Key = File path/name inside bucket

Example:

Bucket: codewithvenu-documents
Object Key: uploads/profile/venu.png

S3 Core Concepts

Concept Meaning
Bucket Top-level container for objects
Object File stored in S3
Key Unique object path inside bucket
Region AWS region where bucket is created
Storage Class Cost and access pattern option
Versioning Keeps multiple versions of same object
Lifecycle Rule Automatically moves/deletes old objects
Presigned URL Temporary secure URL to access object

High Level Architecture

flowchart TD
    U[User]
    UI[Frontend Application]
    API[Spring Boot API]
    SDK[AWS SDK for Java]
    S3[(Amazon S3 Bucket)]
    DB[(Database Metadata)]

    U --> UI
    UI --> API
    API --> SDK
    SDK --> S3
    API --> DB

File Upload Flow

flowchart LR
    A[User Selects File]
    B[Frontend Sends Multipart Request]
    C[Spring Boot Receives File]
    D[Validate File]
    E[Upload to S3]
    F[Save Metadata]
    G[Return Response]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G

Real-Time Use Case

Assume you are building a blog or learning platform.

Users or admins upload files:

Blog thumbnail image
PDF notes
Architecture diagram
Resume
Invoice
Excel sheet

Instead of storing files inside the application server, we store them in S3.

Why?

  • EC2/container file system is temporary
  • S3 is durable
  • S3 scales automatically
  • S3 integrates with CloudFront
  • S3 supports lifecycle rules
  • S3 supports encryption and access control

Prerequisites

You need:

AWS Account
AWS CLI configured
Java 17 or Java 21
Maven
Spring Boot 3.x
AWS SDK for Java 2.x
S3 bucket
IAM user or IAM role

Verify AWS CLI:

aws --version

Output:

aws-cli/2.x.x

Verify identity:

aws sts get-caller-identity

Output:

{
  "UserId": "AIDAEXAMPLE",
  "Account": "123456789012",
  "Arn": "arn:aws:iam::123456789012:user/codewithvenu"
}

Step 1: Create S3 Bucket

Using AWS Console:

AWS Console
→ S3
→ Create bucket

Input:

Bucket name: codewithvenu-springboot-s3-demo
Region: us-east-1
Block public access: Enabled
Bucket versioning: Optional
Default encryption: Enabled

Using AWS CLI:

aws s3api create-bucket \
  --bucket codewithvenu-springboot-s3-demo \
  --region us-east-1

Output:

{
  "Location": "/codewithvenu-springboot-s3-demo"
}

Validate:

aws s3 ls

Output:

2026-06-26 10:00:00 codewithvenu-springboot-s3-demo

Step 2: Create Spring Boot Project

Create project with:

Spring Web
Validation
Spring Boot Actuator
Java 17
Maven

Project structure:

springboot-s3-demo
 ┣ src/main/java/com/codewithvenu/s3
 ┃ ┣ S3DemoApplication.java
 ┃ ┣ config
 ┃ ┃ ┗ AwsS3Config.java
 ┃ ┣ controller
 ┃ ┃ ┗ S3FileController.java
 ┃ ┣ service
 ┃ ┃ ┗ S3FileService.java
 ┃ ┗ dto
 ┃   ┗ FileUploadResponse.java
 ┣ src/main/resources
 ┃ ┗ application.yml
 ┗ pom.xml

Step 3: Add Maven Dependencies

Update pom.xml:

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

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

    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>s3-presigner</artifactId>
        <version>2.25.60</version>
    </dependency>

</dependencies>

Step 4: Configure application.yml

server:
  port: 8080

spring:
  application:
    name: springboot-s3-demo
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

aws:
  region: us-east-1
  s3:
    bucket-name: codewithvenu-springboot-s3-demo

Important:

Do not store AWS access key and secret key inside application.yml.

Avoid:

aws:
  access-key: AKIAxxxx
  secret-key: xxxxx

Step 5: Configure AWS Credentials Locally

For local development:

aws configure

Input:

AWS Access Key ID: your-access-key
AWS Secret Access Key: your-secret-key
Default region name: us-east-1
Default output format: json

AWS SDK uses default credential provider chain.

It can read credentials from:

Environment variables
AWS credentials file
IAM role
ECS task role
EC2 instance profile
EKS IRSA

AWS Credentials Flow

flowchart TD
    APP[Spring Boot App Starts]
    SDK[AWS SDK Default Credentials Provider]
    ENV[Environment Variables]
    PROFILE[AWS Profile File]
    ROLE[IAM Role]
    S3[Amazon S3]

    APP --> SDK
    SDK --> ENV
    SDK --> PROFILE
    SDK --> ROLE
    SDK --> S3

Step 6: Create S3 Client Config

Create:

src/main/java/com/codewithvenu/s3/config/AwsS3Config.java
package com.codewithvenu.s3.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.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AwsS3Config {

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

    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .region(Region.of(awsRegion))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build();
    }

    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.builder()
                .region(Region.of(awsRegion))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build();
    }
}

Step 7: Create Response DTO

Create:

src/main/java/com/codewithvenu/s3/dto/FileUploadResponse.java
package com.codewithvenu.s3.dto;

public class FileUploadResponse {

    private String fileName;
    private String bucketName;
    private String objectKey;
    private String contentType;
    private long size;
    private String message;

    public FileUploadResponse(
            String fileName,
            String bucketName,
            String objectKey,
            String contentType,
            long size,
            String message
    ) {
        this.fileName = fileName;
        this.bucketName = bucketName;
        this.objectKey = objectKey;
        this.contentType = contentType;
        this.size = size;
        this.message = message;
    }

    public String getFileName() {
        return fileName;
    }

    public String getBucketName() {
        return bucketName;
    }

    public String getObjectKey() {
        return objectKey;
    }

    public String getContentType() {
        return contentType;
    }

    public long getSize() {
        return size;
    }

    public String getMessage() {
        return message;
    }
}

Step 8: Create S3 Service

Create:

src/main/java/com/codewithvenu/s3/service/S3FileService.java
package com.codewithvenu.s3.service;

import com.codewithvenu.s3.dto.FileUploadResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
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.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;

import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
public class S3FileService {

    private final S3Client s3Client;
    private final S3Presigner s3Presigner;

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

    public S3FileService(S3Client s3Client, S3Presigner s3Presigner) {
        this.s3Client = s3Client;
        this.s3Presigner = s3Presigner;
    }

    public FileUploadResponse uploadFile(MultipartFile file) throws IOException {

        validateFile(file);

        String objectKey = generateObjectKey(file.getOriginalFilename());

        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .contentType(file.getContentType())
                .contentLength(file.getSize())
                .build();

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

        return new FileUploadResponse(
                file.getOriginalFilename(),
                bucketName,
                objectKey,
                file.getContentType(),
                file.getSize(),
                "File uploaded successfully"
        );
    }

    public byte[] downloadFile(String objectKey) {

        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build();

        ResponseBytes<GetObjectResponse> responseBytes =
                s3Client.getObjectAsBytes(getObjectRequest);

        return responseBytes.asByteArray();
    }

    public String deleteFile(String objectKey) {

        DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build();

        s3Client.deleteObject(deleteObjectRequest);

        return "File deleted successfully: " + objectKey;
    }

    public List<String> listFiles(String prefix) {

        ListObjectsV2Request request = ListObjectsV2Request.builder()
                .bucket(bucketName)
                .prefix(prefix == null ? "" : prefix)
                .build();

        ListObjectsV2Response response = s3Client.listObjectsV2(request);

        return response.contents()
                .stream()
                .map(s3Object -> s3Object.key())
                .toList();
    }

    public URL generatePresignedUrl(String objectKey) {

        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(objectKey)
                .build();

        GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(10))
                .getObjectRequest(getObjectRequest)
                .build();

        return s3Presigner.presignGetObject(presignRequest).url();
    }

    private void validateFile(MultipartFile file) {

        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("File must not be empty");
        }

        if (file.getSize() > 10 * 1024 * 1024) {
            throw new IllegalArgumentException("File size must be less than 10MB");
        }

        String contentType = file.getContentType();

        if (contentType == null) {
            throw new IllegalArgumentException("File content type is missing");
        }

        List<String> allowedTypes = List.of(
                MediaType.IMAGE_JPEG_VALUE,
                MediaType.IMAGE_PNG_VALUE,
                MediaType.APPLICATION_PDF_VALUE,
                "text/plain"
        );

        if (!allowedTypes.contains(contentType)) {
            throw new IllegalArgumentException("Only JPG, PNG, PDF, and TXT files are allowed");
        }
    }

    private String generateObjectKey(String originalFileName) {

        String timestamp = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

        String safeFileName = originalFileName == null
                ? "unknown-file"
                : originalFileName.replaceAll("[^a-zA-Z0-9\\.\\-_]", "_");

        return "uploads/" + timestamp + "-" + safeFileName;
    }
}

Step 9: Create REST Controller

Create:

src/main/java/com/codewithvenu/s3/controller/S3FileController.java
package com.codewithvenu.s3.controller;

import com.codewithvenu.s3.dto.FileUploadResponse;
import com.codewithvenu.s3.service.S3FileService;
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;

import java.net.URL;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/s3")
public class S3FileController {

    private final S3FileService s3FileService;

    public S3FileController(S3FileService s3FileService) {
        this.s3FileService = s3FileService;
    }

    @PostMapping("/upload")
    public ResponseEntity<FileUploadResponse> uploadFile(
            @RequestParam("file") MultipartFile file
    ) throws Exception {

        FileUploadResponse response = s3FileService.uploadFile(file);
        return ResponseEntity.ok(response);
    }

    @GetMapping("/download")
    public ResponseEntity<byte[]> downloadFile(
            @RequestParam String key
    ) {

        byte[] fileContent = s3FileService.downloadFile(key);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"downloaded-file\"")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(fileContent);
    }

    @DeleteMapping("/delete")
    public ResponseEntity<Map<String, String>> deleteFile(
            @RequestParam String key
    ) {

        String message = s3FileService.deleteFile(key);
        return ResponseEntity.ok(Map.of("message", message));
    }

    @GetMapping("/list")
    public ResponseEntity<List<String>> listFiles(
            @RequestParam(required = false) String prefix
    ) {

        List<String> files = s3FileService.listFiles(prefix);
        return ResponseEntity.ok(files);
    }

    @GetMapping("/presigned-url")
    public ResponseEntity<Map<String, String>> generatePresignedUrl(
            @RequestParam String key
    ) {

        URL url = s3FileService.generatePresignedUrl(key);
        return ResponseEntity.ok(Map.of("url", url.toString()));
    }
}

Step 10: Create Main Application Class

package com.codewithvenu.s3;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class S3DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(S3DemoApplication.class, args);
    }
}

Step 11: Run Application

Input:

mvn spring-boot:run

Output:

Tomcat started on port 8080
Started S3DemoApplication

Step 12: Upload File API

Create sample file:

echo "Hello CodeWithVenu S3 Demo" > hello.txt

Input:

curl -X POST http://localhost:8080/api/s3/upload \
  -F "[email protected]"

Output:

{
  "fileName": "hello.txt",
  "bucketName": "codewithvenu-springboot-s3-demo",
  "objectKey": "uploads/20260626103045-hello.txt",
  "contentType": "text/plain",
  "size": 28,
  "message": "File uploaded successfully"
}

Validate from AWS CLI:

aws s3 ls s3://codewithvenu-springboot-s3-demo/uploads/

Output:

2026-06-26 10:30:45         28 20260626103045-hello.txt

Step 13: List Files API

Input:

curl "http://localhost:8080/api/s3/list?prefix=uploads/"

Output:

[
  "uploads/20260626103045-hello.txt"
]

Step 14: Download File API

Input:

curl -o downloaded-hello.txt \
"http://localhost:8080/api/s3/download?key=uploads/20260626103045-hello.txt"

Output:

File downloaded as downloaded-hello.txt

Verify:

cat downloaded-hello.txt

Output:

Hello CodeWithVenu S3 Demo

Step 15: Generate Presigned URL

Input:

curl "http://localhost:8080/api/s3/presigned-url?key=uploads/20260626103045-hello.txt"

Output:

{
  "url": "https://codewithvenu-springboot-s3-demo.s3.amazonaws.com/uploads/20260626103045-hello.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256..."
}

Open the URL in browser.

Expected output:

File is downloaded or displayed depending on browser and content type.

Presigned URL Flow

flowchart LR
    U[User]
    API[Spring Boot API]
    SDK[AWS SDK Presigner]
    URL[Temporary Presigned URL]
    S3[(Amazon S3 Object)]

    U --> API
    API --> SDK
    SDK --> URL
    U --> URL
    URL --> S3

Step 16: Delete File API

Input:

curl -X DELETE \
"http://localhost:8080/api/s3/delete?key=uploads/20260626103045-hello.txt"

Output:

{
  "message": "File deleted successfully: uploads/20260626103045-hello.txt"
}

Validate:

aws s3 ls s3://codewithvenu-springboot-s3-demo/uploads/

Output:

No object should be listed for the deleted key.

Step 17: IAM Policy for S3 Access

For local demo, IAM user needs limited S3 access.

Use least privilege policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SpringBootS3BucketAccess",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::codewithvenu-springboot-s3-demo"
      ]
    },
    {
      "Sid": "SpringBootS3ObjectAccess",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::codewithvenu-springboot-s3-demo/*"
      ]
    }
  ]
}

Runtime Authentication Best Practice

Local development:

AWS CLI profile or environment variables

Production on EC2:

IAM Instance Profile

Production on ECS:

ECS Task Role

Production on EKS:

IAM Role for Service Account

Production on Lambda:

Lambda Execution Role

Production S3 Architecture

flowchart TD
    U[User]
    UI[Frontend]
    API[Spring Boot API]
    IAM[IAM Role]
    S3[(Private S3 Bucket)]
    KMS[AWS KMS]
    DB[(Metadata Database)]
    CF[CloudFront CDN]

    U --> UI
    UI --> API
    API --> IAM
    API --> S3
    S3 --> KMS
    API --> DB
    CF --> S3

Common Errors and Fixes

Error 1: Unable to Load Credentials

Error:

Unable to load credentials from any of the providers

Fix:

aws configure
aws sts get-caller-identity

For production, attach IAM role to the compute service.


Error 2: Access Denied

Error:

software.amazon.awssdk.services.s3.model.S3Exception: Access Denied

Fix:

Check IAM permissions:

s3:PutObject
s3:GetObject
s3:DeleteObject
s3:ListBucket

Also check bucket policy and encryption policy.


Error 3: Bucket Not Found

Error:

NoSuchBucket

Fix:

aws s3 ls

Check bucket name and region.


Error 4: File Size Too Large

Fix application config:

spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 20MB

Also validate file size inside service layer.


Error 5: Region Mismatch

Fix:

aws:
  region: us-east-1

Make sure region matches bucket region.


Best Practices

  • Keep S3 buckets private by default
  • Do not hardcode AWS keys
  • Use IAM roles in production
  • Use least privilege IAM policy
  • Enable default encryption
  • Use KMS for sensitive files
  • Validate file type and size
  • Generate unique object keys
  • Store file metadata in database
  • Use presigned URLs for secure temporary access
  • Use CloudFront for public/static content
  • Enable bucket versioning if needed
  • Configure lifecycle policies
  • Enable S3 server access logs or CloudTrail
  • Avoid exposing raw bucket URLs publicly
  • Use virus scanning workflow for user-uploaded files

S3 Upload Best Practice Flow

flowchart TD
    U[User Uploads File]
    API[Spring Boot API]
    VALIDATE[Validate Size and Type]
    SCAN[Optional Virus Scan]
    S3[Upload to Private S3]
    DB[Save Metadata]
    URL[Generate Presigned URL]

    U --> API
    API --> VALIDATE
    VALIDATE --> SCAN
    SCAN --> S3
    S3 --> DB
    DB --> URL

Interview Questions

What is Amazon S3?

Amazon S3 is AWS object storage used to store and retrieve files, backups, images, documents, logs, and large-scale data.

What is a bucket?

A bucket is a top-level container that stores S3 objects.

What is an object key?

An object key is the unique path or name of a file inside an S3 bucket.

How does Spring Boot connect to S3?

Spring Boot connects to S3 using AWS SDK for Java. We create an S3Client bean and call methods like putObject, getObject, deleteObject, and listObjectsV2.

Why should we use presigned URLs?

Presigned URLs provide temporary access to private S3 objects without making the bucket public.

Should S3 bucket be public?

No. In most enterprise applications, S3 buckets should remain private. Access should be controlled using IAM roles, bucket policies, CloudFront, or presigned URLs.

Where should AWS credentials be stored?

For local development, use AWS CLI profile or environment variables. For production, use IAM roles.


Summary

In this article, we integrated Spring Boot with Amazon S3.

We covered:

  • S3 bucket creation
  • AWS SDK dependency
  • S3 client configuration
  • Upload file API
  • Download file API
  • Delete file API
  • List files API
  • Presigned URL generation
  • IAM policy
  • Input and output examples
  • Common errors
  • Best practices

Amazon S3 is a foundational AWS service for Java developers. It is commonly used in enterprise applications for file storage, reports, document management, data ingestion, backups, and static assets.


Loading likes...

Comments

Share a question, correction, or practical insight about this article.

Loading approved comments...