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.
Comments
Share a question, correction, or practical insight about this article.
Checking login status...
Loading approved comments...