Intermediate · Build

Object Storage & Presigned URLs

Move files behind S3-compatible object storage, upload through the app or direct presigned PUTs, and keep private objects genuinely private.

Primary source: Spring Boot 3.5 Reference: Uploading Files

35 min

Lesson 2 of 2

1d streak

The Architectural Question

Public buckets and long-lived URLs leak every private document to whoever holds the link; hard-coding S3 into services makes local development and testing painful.

What this guide proves

  • Abstract storage behind an interface so local and cloud swap freely
  • Upload with the SDK or hand clients time-boxed presigned URLs
  • Serve downloads through expiring presigned GETs after authorization

AWS SDK v2 S3 Client Integration with MinIO & AWS

In modern cloud architectures, storing binary files on local application container filesystems is an anti-pattern. Container filesystems are ephemeral, non-shared, and limited in disk capacity. Instead, applications store files in S3-compatible object storage (AWS S3, MinIO, Cloudflare R2).

Spring Boot integrates with AWS SDK for Java v2 (S3Client and S3Presigner) with automatic credential provider chains and connection pooling.

src/main/java/com/example/storage/S3StorageConfig.java
package com.example.storage;

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;

import java.net.URI;

@Configuration(proxyBeanMethods = false)
public class S3StorageConfig {

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

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

Direct Browser-to-S3 Uploads via Presigned URLs

Having client web browsers upload 500MB videos directly to your Spring Boot application server saturates backend bandwidth, blocks web threads, and increases cloud egress costs.

The Presigned URL pattern offloads data transfer entirely: (1) Browser asks Spring Boot for an upload token; (2) Spring Boot verifies user permissions and signs a time-limited S3 PUT URL; (3) Browser uploads bytes directly to S3; (4) Browser notifies Spring Boot upon completion.

src/main/java/com/example/storage/PresignedUrlService.java
package com.example.storage;

import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.time.Duration;

@Service
public class PresignedUrlService {

    private final S3Presigner s3Presigner;
    private static final String BUCKET = "enterprise-documents";

    public PresignedUrlService(S3Presigner s3Presigner) {
        this.s3Presigner = s3Presigner;
    }

    public String generateUploadUrl(String documentId, String contentType) {
        PutObjectRequest objectRequest = PutObjectRequest.builder()
            .bucket(BUCKET)
            .key("uploads/" + documentId)
            .contentType(contentType)
            .build();

        // Generate time-limited upload URL valid for 15 minutes
        PresignedPutObjectRequest presigned = s3Presigner.presignPutObject(builder -> builder
            .signatureDuration(Duration.ofMinutes(15))
            .putObjectRequest(objectRequest)
        );

        return presigned.url().toString();
    }
}

Bucket Lifecycle Rules & Antivirus Workflows

Cloud storage must be governed by automated lifecycle rules. Transition staging uploads to cheaper Glacier storage after 90 days and permanently expire incomplete multipart uploads after 7 days.

Integrate S3 Event Notifications with an AWS Lambda or asynchronous worker to trigger ClamAV virus scanning on uploaded files before marking them as approved for end-user downloading.

Testing Presigned Uploads and Expiration Enforcement

Test direct presigned upload using curl with HTTP PUT:

snippet.bash
# 1. Request presigned URL from Spring Boot
UPLOAD_URL=$(curl -s http://localhost:8080/api/documents/doc-101/upload-url | jq -r .url)

# 2. Upload file directly to S3 using the presigned URL
curl -i -X PUT -T report.pdf -H "Content-Type: application/pdf" "$UPLOAD_URL"
# EXPECTED: HTTP/1.1 200 OK (Uploaded directly to S3 without hitting Spring server!)

# 3. Verify upload expires after 15 minutes
# Attempting upload after expiration returns HTTP 403 Forbidden with RequestExpired

Active Recall Checkpoint

Why does uploading files directly to S3 via presigned URLs improve backend scalability?

Hint: Think about server network bandwidth and thread allocation under concurrent file uploads.

Show answer & explanation

The backend server only generates a cryptographically signed URL string (< 1KB), offloading all multi-megabyte file streaming, bandwidth consumption, and socket connections to AWS S3 infrastructure.

Runtime Architecture & Flow

Spring MVC · runtime visualization

Object Storage & Presigned URLs

Dispatcher

Abstract storage behind an interface so local and cloud swap freely

Binding

Upload with the SDK or hand clients time-boxed presigned URLs

Use case

Serve downloads through expiring presigned GETs after authorization

Response

Swap local attachment storage for MinIO, then issue a 60-second presigned download URL and watch it expire.

Working Implementation

Code Walkthrough

Swap local attachment storage for MinIO, then issue a 60-second presigned download URL and watch it expire.

PresignedUrlStorageLab.java
package com.example.storage;

import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.time.Duration;

@Service
public class PresignedUrlStorageLab {

    private final S3Presigner s3Presigner;

    public PresignedUrlStorageLab(S3Presigner s3Presigner) {
        this.s3Presigner = s3Presigner;
    }

    public String createUploadUrl(String bucket, String key, String contentType) {
        var putRequest = PutObjectRequest.builder()
            .bucket(bucket)
            .key(key)
            .contentType(contentType)
            .build();

        PresignedPutObjectRequest presigned = s3Presigner.presignPutObject(builder -> builder
            .signatureDuration(Duration.ofMinutes(15))
            .putObjectRequest(putRequest)
        );

        return presigned.url().toString();
    }
}
Presigned URLs offload multi-megabyte file uploads directly to S3, bypassing backend JVM memory and network bandwidth.
Signed URLs expire strictly after the configured duration (e.g. 15 minutes), preventing replay attacks.
Bucket lifecycle rules automatically purge unreferenced staging uploads after a retention window.

Synthesis & Production Verification

Deliberate Failure Condition

  1. Break: Generate presigned URLs valid for seven days and share them in emails
  2. Observe: Forwarded links keep granting access long after permissions changed
  3. Repair: Presign narrowly: shortest workable lifetime, issued only after your authorization check, since possession of the URL becomes the credential.

Production Acceptance Criteria

  • Storage implementation swaps via configuration alone
  • Downloads require app authorization then redirect to a presigned URL
  • Expired URLs fail closed after the configured lifetime

Architectural & Technical Review

Core Interview Questions

Q01Why are presigned S3 URLs preferred over streaming large files through Spring Boot application servers?Show answer

Direct uploads to S3 eliminate backend network bandwidth consumption and socket thread starvation. The application server only generates a signed URL string (< 1KB), offloading multi-megabyte data transfer to cloud object storage.

Q02How do AWS SDK v2 S3Presigner time-limited signatures enhance upload security?Show answer

Presigned URLs embed cryptographic HMAC signatures with an explicit expiration window (e.g. 15 minutes) and restricted HTTP method/bucket path. Any request attempted after expiration or targeting another path is rejected by AWS.

Q03What automated operational policies should govern production S3 storage buckets?Show answer

Configure S3 Lifecycle Rules to abort incomplete multipart uploads after 7 days, transition older documents to Infrequent Access / Glacier, and trigger asynchronous virus scanning upon new file upload events.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Streaming large video or archive uploads through backend application containers — Consumes backend network bandwidth and blocks worker threads, degrading API responsiveness.
  • Pitfall: Generating presigned URLs with indefinite or excessive expiration durations — Extends the window of vulnerability if a signed URL is intercepted or leaked in browser histories.

Module Certification Quiz

Prove Module Mastery

Comprehensive question set covering all lessons in this module.

Take Module Quiz

Primary Documentation & Specifications

Supplementary Module Resources