Home Java Spring Cloud AWS S3: File Storage and Download with Amazon S3
Advanced 4 min · July 14, 2026

Spring Cloud AWS S3: File Storage and Download with Amazon S3

Learn to integrate Spring Cloud AWS with Amazon S3 for robust file storage and download.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+
  • Spring Boot 3.x
  • AWS account with S3 bucket created
  • AWS credentials (access key and secret key) for local development
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use the spring-cloud-starter-aws dependency to integrate S3 with Spring Boot.
  • Leverage AmazonS3 bean for upload/download; configure region and credentials via application.yml.
  • For large files, use multipart upload to avoid memory issues.
  • Implement pre-signed URLs for secure, time-limited access.
  • Handle S3 exceptions like AmazonS3Exception with proper retry logic.
✦ Definition~90s read
What is Spring Cloud AWS S3?

Spring Cloud AWS S3 is a Spring Boot integration that simplifies interacting with Amazon S3 for file storage and retrieval using familiar Spring patterns.

Think of Amazon S3 as a massive, secure filing cabinet in the cloud.
Plain-English First

Think of Amazon S3 as a massive, secure filing cabinet in the cloud. Spring Cloud AWS gives you a set of keys to this cabinet. You can store files (upload), retrieve them (download), and even give temporary access to others (pre-signed URLs). It's like having a robot assistant that handles all the heavy lifting.

File storage is a fundamental requirement for nearly every application. Whether you're handling user uploads in a SaaS platform, storing logs for analytics, or serving static assets for a content management system, you need a reliable, scalable, and cost-effective solution. Amazon S3 is the gold standard for object storage, and Spring Cloud AWS provides first-class integration to make it painless from your Java applications.

But let's be honest: the official documentation glosses over the real-world gotchas. I've seen production outages caused by misconfigured credentials, silent failures from region mismatches, and memory blow-ups from streaming large files incorrectly. In this article, I'll share battle-tested patterns for uploading and downloading files with S3 using Spring Cloud AWS. We'll cover synchronous operations, multipart uploads for large files, and secure access via pre-signed URLs. I'll also walk through a real production incident where a simple timeout configuration saved a fintech startup from a costly outage.

By the end, you'll know not just how to use the API, but how to do it right in production. Let's get to it.

Setting Up Spring Cloud AWS with S3

First, add the necessary dependencies. I recommend using the latest stable version – as of 2024, that's spring-cloud-starter-aws 2.4.4. Don't use the old 1.x versions; they lack critical fixes.

``xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-aws</artifactId> <version>2.4.4</version> </dependency> ``

Then configure your AWS credentials and region. Never hardcode credentials! Use IAM roles in production, environment variables for local dev, or the default credential chain.

``yaml cloud: aws: region: static: us-east-1 credentials: access-key: ${AWS_ACCESS_KEY_ID} secret-key: ${AWS_SECRET_ACCESS_KEY} ``

``java @Autowired private AmazonS3 amazonS3; ``

But wait – there's a trap. The default AmazonS3 client uses the AWS SDK v1. For new projects, consider using the AWS SDK v2 with the s3 module. Spring Cloud AWS doesn't yet support v2 natively, but you can manually configure it. I'll show both approaches.

S3Config.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

    @Bean
    public AmazonS3 amazonS3() {
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new DefaultAWSCredentialsProviderChain())
                .withRegion(Regions.US_EAST_1)
                .build();
    }
}
⚠ Region Mismatch Gotcha
📊 Production Insight
In production, always validate the bucket region at startup. I once saw a team spend days debugging why their uploads worked locally but failed in staging – the bucket was in eu-west-1 but the config said us-east-1.
🎯 Key Takeaway
Configure the S3 client with explicit region and use the default credential chain for production. Avoid hardcoding credentials.

Uploading Files to S3

Uploading a file is straightforward with putObject. But the devil is in the details. Here's a robust method that handles streams properly:

```java public String uploadFile(String bucketName, String key, InputStream inputStream, long contentLength, String contentType) { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentLength(contentLength); metadata.setContentType(contentType);

PutObjectRequest request = new PutObjectRequest(bucketName, key, inputStream, metadata); PutObjectResult result = amazonS3.putObject(request); return result.getETag(); } ```

Important: Always set Content-Length if you know it. If you don't, the SDK buffers the entire stream into memory, which can cause OOM for large files. For unknown length streams, use multipart upload.

``java public String uploadLargeFile(String bucketName, String key, File file) { TransferManager transferManager = TransferManagerBuilder.standard() .withS3Client(amazonS3) .build(); Upload upload = transferManager.upload(bucketName, key, file); try { upload.waitForCompletion(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Upload interrupted", e); } return upload.getETag(); } ``

TransferManager handles multipart uploads automatically based on a size threshold (default 16 MB). This avoids memory issues and provides progress tracking.

FileUploadService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.PutObjectResult;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferManagerBuilder;
import com.amazonaws.services.s3.transfer.Upload;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.InputStream;

@Service
public class FileUploadService {

    private final AmazonS3 amazonS3;

    public FileUploadService(AmazonS3 amazonS3) {
        this.amazonS3 = amazonS3;
    }

    public String uploadFile(String bucketName, String key, InputStream inputStream, long contentLength, String contentType) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(contentLength);
        metadata.setContentType(contentType);

        PutObjectRequest request = new PutObjectRequest(bucketName, key, inputStream, metadata);
        PutObjectResult result = amazonS3.putObject(request);
        return result.getETag();
    }

    public String uploadLargeFile(String bucketName, String key, File file) {
        TransferManager transferManager = TransferManagerBuilder.standard()
                .withS3Client(amazonS3)
                .build();
        Upload upload = transferManager.upload(bucketName, key, file);
        try {
            upload.waitForCompletion();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Upload interrupted", e);
        }
        return upload.getETag();
    }
}
💡Streaming Best Practice
📊 Production Insight
Set a reasonable multipart upload threshold. The default 16 MB is fine for most cases, but if you're uploading many small files, you might want to increase it to avoid the overhead of multipart.
🎯 Key Takeaway
Use putObject for small files with known content length. Use TransferManager for large files to leverage multipart uploads and avoid memory issues.

Downloading Files from S3

Downloading is similarly straightforward. Use getObject to get an S3Object and read the content stream:

``java public S3ObjectInputStream downloadFile(String bucketName, String key) { S3Object s3Object = amazonS3.getObject(bucketName, key); return s3Object.getObjectContent(); } ``

But again, be careful with streams. The S3ObjectInputStream must be closed to avoid connection leaks. I always wrap it in a try-with-resources or ensure the caller closes it.

For large files, consider using GetObjectRequest with range headers to download in chunks:

``java public InputStream downloadFileRange(String bucketName, String key, long start, long end) { GetObjectRequest request = new GetObjectRequest(bucketName, key); request.setRange(start, end); S3Object s3Object = amazonS3.getObject(request); return s3Object.getObjectContent(); } ``

This is useful for resumable downloads or streaming large files to clients without buffering the entire object in memory.

FileDownloadService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import org.springframework.stereotype.Service;
import java.io.InputStream;

@Service
public class FileDownloadService {

    private final AmazonS3 amazonS3;

    public FileDownloadService(AmazonS3 amazonS3) {
        this.amazonS3 = amazonS3;
    }

    public S3ObjectInputStream downloadFile(String bucketName, String key) {
        S3Object s3Object = amazonS3.getObject(bucketName, key);
        return s3Object.getObjectContent();
    }

    public InputStream downloadFileRange(String bucketName, String key, long start, long end) {
        GetObjectRequest request = new GetObjectRequest(bucketName, key);
        request.setRange(start, end);
        S3Object s3Object = amazonS3.getObject(request);
        return s3Object.getObjectContent();
    }
}
🔥Stream Closing is Critical
📊 Production Insight
For serving files to users, use pre-signed URLs instead of streaming through your server. It reduces load and improves performance. We'll cover that next.
🎯 Key Takeaway
Always close S3 object streams. Use range downloads for large files to avoid memory pressure.

Generating Pre-Signed URLs for Secure Access

Pre-signed URLs are a game-changer. They allow you to grant temporary access to a specific S3 object without exposing your AWS credentials. This is perfect for file downloads in web applications.

``java public URL generatePresignedUrl(String bucketName, String key, int expirationInMinutes) { Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(expirationInMinutes)); GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, key) .withMethod(HttpMethod.GET) .withExpiration(expiration); return amazonS3.generatePresignedUrl(request); } ``

This URL can be used by anyone to download the object within the expiration period. You can also generate PUT pre-signed URLs to allow clients to upload directly to S3, bypassing your server entirely – great for large uploads.

Security Note: The URL includes your secret access key in a signed signature. Never log it or expose it in error messages. Also, set a short expiration (e.g., 5-15 minutes) to minimize risk.

PresignedUrlService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import org.springframework.stereotype.Service;
import java.net.URL;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
public class PresignedUrlService {

    private final AmazonS3 amazonS3;

    public PresignedUrlService(AmazonS3 amazonS3) {
        this.amazonS3 = amazonS3;
    }

    public URL generatePresignedUrl(String bucketName, String key, int expirationInMinutes) {
        Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(expirationInMinutes));
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, key)
                .withMethod(HttpMethod.GET)
                .withExpiration(expiration);
        return amazonS3.generatePresignedUrl(request);
    }
}
⚠ Pre-signed URL Gotcha
📊 Production Insight
In a multi-tenant SaaS, you can generate pre-signed URLs for each tenant's files, with expiration tied to their session. This isolates access without per-request IAM checks.
🎯 Key Takeaway
Use pre-signed URLs for secure, time-limited access to S3 objects. Avoid streaming through your server when possible.

What the Official Docs Won't Tell You

The official Spring Cloud AWS documentation is decent, but it leaves out several hard-earned lessons. Here are the ones that have bitten me and my teams:

1. The Default Credential Chain is Your Friend, But Test It

The DefaultAWSCredentialsProviderChain tries environment variables, system properties, profile config, and instance profile. In production, use instance profiles (EC2) or task roles (ECS). But I've seen cases where the chain picks up an expired profile from ~/.aws/credentials and causes intermittent failures. Always test your credential resolution in the target environment.

2. Multipart Upload Cleanup

If a multipart upload fails, S3 retains the parts. They accumulate and cost money. Use TransferManager which automatically aborts incomplete uploads, but if you use the low-level API, you must handle AbortMultipartUpload yourself. Set a lifecycle policy on your bucket to expire incomplete multipart uploads after a few days.

3. S3 Consistency Model

S3 offers read-after-write consistency for PUTs of new objects, but eventual consistency for overwrites and DELETEs. If you update an object and immediately read it, you might get the old version. This is rare but can cause bugs in distributed systems. Use versioning or avoid overwrites if consistency is critical.

4. Pre-signed URLs and KMS Encryption

If your bucket uses AWS KMS for encryption, pre-signed URLs require the generator to have kms:GenerateDataKey permission. Also, the URL might not work if the requester doesn't have KMS decrypt permissions. Test this early.

5. Client Configuration Tuning

The default ClientConfiguration uses a connection timeout of 10 seconds and socket timeout of 50 seconds. For large file uploads, you'll need to increase these. Set them explicitly:

``java ClientConfiguration clientConfig = new ClientConfiguration(); clientConfig.setConnectionTimeout(30_000); clientConfig.setSocketTimeout(120_000); AmazonS3 s3Client = AmazonS3ClientBuilder.standard() .withClientConfiguration(clientConfig) .build(); ``

Ignoring this caused a production incident I'll never forget.

S3ClientWithTimeout.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.amazonaws.ClientConfiguration;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

public class S3ClientWithTimeout {

    public static AmazonS3 createClient() {
        ClientConfiguration clientConfig = new ClientConfiguration();
        clientConfig.setConnectionTimeout(30_000); // 30 seconds
        clientConfig.setSocketTimeout(120_000); // 2 minutes
        return AmazonS3ClientBuilder.standard()
                .withClientConfiguration(clientConfig)
                .build();
    }
}
💡Lifecycle Policy for Multipart Uploads
📊 Production Insight
I once worked with a team that used default timeouts and wondered why large file uploads failed in production. Their network latency was higher than expected. After increasing timeouts, the issue vanished.
🎯 Key Takeaway
Configure timeouts explicitly, handle multipart upload cleanup, and be aware of S3's consistency model. Test credential resolution in each environment.

Error Handling and Retries

S3 operations can fail for many reasons: network issues, throttling, permissions, etc. The AWS SDK has a built-in retry mechanism, but it's not always sufficient. Here's my approach:

1. Use Exponential Backoff

The SDK retries up to 3 times by default with exponential backoff. That's usually enough, but for critical operations, consider implementing your own retry with a circuit breaker pattern using Spring Retry or Resilience4j.

2. Handle Specific Exceptions

  • AmazonS3Exception: General S3 error. Check the error code.
  • AmazonServiceException: Service-side error (e.g., 503). Retry.
  • AmazonClientException: Client-side error (e.g., connection timeout). Retry.
  • SdkClientException: SDK-level error.

3. Log and Monitor

Always log S3 errors with enough context (bucket, key, error code). Set up CloudWatch alarms for high error rates.

``java @Retryable(value = {AmazonServiceException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public String uploadFileWithRetry(String bucketName, String key, InputStream inputStream, long contentLength, String contentType) { return uploadFile(bucketName, key, inputStream, contentLength, contentType); } ``

But be careful: retrying idempotent operations (like GET) is safe, but for PUTs, ensure your upload is idempotent or handle duplicates.

RetryableUploadService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.amazonaws.AmazonServiceException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.io.InputStream;

@Service
public class RetryableUploadService {

    private final FileUploadService fileUploadService;

    public RetryableUploadService(FileUploadService fileUploadService) {
        this.fileUploadService = fileUploadService;
    }

    @Retryable(value = {AmazonServiceException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
    public String uploadFileWithRetry(String bucketName, String key, InputStream inputStream, long contentLength, String contentType) {
        return fileUploadService.uploadFile(bucketName, key, inputStream, contentLength, contentType);
    }
}
🔥Idempotency for PUTs
📊 Production Insight
In a high-throughput system, throttling (HTTP 503) is common. Exponential backoff with jitter helps spread retries and avoids thundering herd.
🎯 Key Takeaway
Use Spring Retry for critical S3 operations, but ensure idempotency. Log all S3 errors with sufficient context for debugging.
● Production incidentPOST-MORTEMseverity: high

The Silent Upload Failure at FinTechX

Symptom
Users reported that uploaded PDFs were missing from the portal. No error logs on the application side.
Assumption
The developer assumed the default region (us-east-1) was correct, and that the SDK would throw an error if the bucket was in a different region.
Root cause
The S3 bucket was in eu-west-1 (Ireland), but the application was configured to use us-east-1. The PutObjectRequest succeeded silently because the SDK redirected the request to the correct region, but the response was misinterpreted. The object was stored in the bucket, but the application's subsequent getUrl call used the wrong region endpoint, returning a 301 redirect that the client didn't follow.
Fix
Explicitly set the region in application.yml to match the bucket's region. Added region validation at startup by calling s3Client.getBucketLocation() and comparing it to the configured region.
Key lesson
  • Always explicitly configure the S3 region; never rely on defaults.
  • Validate the bucket region at application startup to catch misconfigurations early.
  • Enable SDK logging to see redirects and error responses.
  • Use getBucketLocation() to programmatically verify the region.
  • Implement health checks that attempt a simple S3 operation (e.g., list objects) to ensure connectivity.
Production debug guideSymptom to Action4 entries
Symptom · 01
File upload succeeds but file is not visible in S3 console.
Fix
Check the bucket region and compare with the region configured in the application. Use aws s3api get-bucket-location --bucket <name> from CLI.
Symptom · 02
Download returns 403 Forbidden.
Fix
Verify IAM permissions for the role/user. Check if the object is encrypted with a custom KMS key and the role lacks kms:Decrypt permission.
Symptom · 03
Upload of large files fails with timeout or OOM.
Fix
Switch to multipart upload. Increase connectionTimeout and socketTimeout in the client configuration. Monitor heap usage with -Xmx flags.
Symptom · 04
Pre-signed URL returns 403 when accessed.
Fix
Check the expiration time (must be in the future). Ensure the URL is generated with the same region as the bucket. Verify the IAM policy allows GetObject for the principal.
★ Quick Debug Cheat SheetImmediate actions for common S3 issues.
Upload returns null ETag
Immediate action
Check if multipart upload is required for the file size.
Commands
aws s3api head-object --bucket <bucket> --key <key>
aws s3api get-bucket-location --bucket <bucket>
Fix now
Enable multipart upload for files > 5GB or enforce it via configuration.
Download hangs indefinitely+
Immediate action
Check network connectivity and firewall rules.
Commands
telnet <bucket>.s3.<region>.amazonaws.com 443
curl -v https://<bucket>.s3.<region>.amazonaws.com/<key>
Fix now
Increase socket timeout in S3 client configuration.
Pre-signed URL returns AccessDenied+
Immediate action
Verify the IAM policy allows GetObject for the generating principal.
Commands
aws sts get-caller-identity
aws iam simulate-principal-policy --policy-source-arn <arn> --action-names s3:GetObject --resource-arns <bucket-arn>
Fix now
Add s3:GetObject permission to the IAM role.
FeatureSpring Cloud AWS (v1 SDK)AWS SDK v2
Dependencyspring-cloud-starter-awssoftware.amazon.awssdk:s3
Region Configurationcloud.aws.region.staticRegion.of() in builder
Multipart UploadTransferManagerS3AsyncClient or S3Client with multipart API
Pre-signed URLsGeneratePresignedUrlRequestPresignedGetObjectRequest
Spring IntegrationAuto-configured beansManual bean configuration
MaturityStable, but v1 SDK deprecatedActive development, recommended for new projects
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
S3Config.java@ConfigurationSetting Up Spring Cloud AWS with S3
FileUploadService.java@ServiceUploading Files to S3
FileDownloadService.java@ServiceDownloading Files from S3
PresignedUrlService.java@ServiceGenerating Pre-Signed URLs for Secure Access
S3ClientWithTimeout.javapublic class S3ClientWithTimeout {What the Official Docs Won't Tell You
RetryableUploadService.java@ServiceError Handling and Retries

Key takeaways

1
Configure S3 client with explicit region and use default credential chain for production.
2
Use TransferManager for large file uploads to leverage multipart uploads.
3
Always close S3 input streams to avoid resource leaks.
4
Prefer pre-signed URLs for file downloads to reduce server load and improve security.
5
Implement retry logic with exponential backoff for resilience against transient failures.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how you would upload a 10 GB file to S3 from a Spring Boot appli...
Q02SENIOR
What is a pre-signed URL and when would you use it?
Q03SENIOR
How do you handle S3 throttling (503 SlowDown errors) in a Spring Boot a...
Q01 of 03SENIOR

Explain how you would upload a 10 GB file to S3 from a Spring Boot application without causing an OutOfMemoryError.

ANSWER
I would use the AWS SDK's TransferManager which automatically uses multipart upload. It splits the file into parts (default 16 MB), uploads them in parallel, and assembles them. This avoids loading the entire file into memory. I'd also configure appropriate timeouts and monitor the upload progress.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
How do I configure S3 client for a specific region?
02
What's the best way to upload large files to S3?
03
How do I generate a pre-signed URL for file download?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Cloud. Mark it forged?

4 min read · try the examples if you haven't

Previous
Spring Cloud AWS RDS: Database Configuration and Connection Management
30 / 34 · Spring Cloud
Next
Spring Cloud AWS EC2: Instance Management and Elastic Compute Integration