JavaSpring BootBackend

Spring Boot File Upload and Download — Complete Guide

Spring Boot File Upload and Download — Complete Guide

Handling spring boot file upload and file download operations is a fundamental requirement for almost every modern web application and RESTful backend service. Whether you are building an e-commerce platform allowing sellers to upload product images, a document management system handling PDF invoices, an HR portal processing resumes, or a social networking app storing user avatars—handling file transfers cleanly, securely, and scalably is essential.

In this comprehensive, production-ready guide, we will build a complete file management REST API using Java 21, Spring Boot 3.x, Spring Data JPA, PostgreSQL, and Gradle. You will learn how to handle single and batch spring boot multipart file upload requests, serve inline browser previews for images, trigger file attachments for downloads, store metadata cleanly in PostgreSQL, enforce strict validation, implement global exception handling, prevent path traversal security vulnerabilities, and prepare your application for production deployment.

For core API building blocks used in this tutorial, see our articles on Spring Boot REST API in 30 Minutes, Spring Boot PostgreSQL CRUD with JPA and Hibernate, and Spring Boot Global Exception Handling with @ControllerAdvice.
Table of Contents

1. Introduction

Why File Upload Matters

Files represent unstructured data. Unlike traditional database fields like usernames or email addresses, files vary vastly in size, format, binary encoding, and security profiles. Handling file uploads requires proper memory management (avoiding loading 500 MB files directly into Java heap memory), validation, and secure disk management.

Real-world Use Cases

  • Media & User Avatars: Profile picture uploads, photo galleries, cover banners.
  • Document Portals: PDF invoices, tax documents, signed contracts, medical records.
  • E-Commerce: High-resolution product images, digital downloads (PDFs, ZIPs).
  • Enterprise Workflows: CSV/Excel spreadsheet imports, bulk report generation.

Supported File Types

Our Spring Boot application will support standard image formats (JPEG, PNG, WEBP), document formats (PDF, DOCX), text files (CSV, TXT), and archives (ZIP), with custom validation to reject unauthorized executable scripts (.sh, .exe, .bat).

2. Understanding MultipartFile

What is MultipartFile?

MultipartFile is a core Spring interface in the org.springframework.web.multipart package. It represents an uploaded file received in a multipart/form-data HTTP request. Spring automatically parses the incoming boundary-delimited HTTP payload and wraps each file in a MultipartFile object.

Key methods provided by MultipartFile:

  • getOriginalFilename(): Returns the filename in the client's filesystem.
  • getContentType(): Returns the MIME type (e.g., image/png, application/pdf).
  • getSize(): Returns the file size in bytes.
  • getBytes(): Returns the file content as a byte array (use with caution for large files).
  • getInputStream(): Provides an InputStream to stream file content efficiently.
  • transferTo(File dest): Convenience method to copy the uploaded file directly to a destination file.

How Spring Boot Handles File Uploads

Under the hood, Spring Boot uses Servlet 3.0+ multipart parsing (or Commons FileUpload). When a request with Content-Type: multipart/form-data hits the server, Spring Boot writes temporary files to a disk directory if the file exceeds a configured threshold, preventing OutOfMemoryError exceptions on your JVM.

3. Project Setup

Generate a new project using Spring Initializr (start.spring.io) or configure your build.gradle manually with Java 21 and Spring Boot 3.3.x.

Gradle Configuration (`build.gradle`)

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.5'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.digitaldrift'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

4. Project Structure

Following modern layered architecture and domain clean structure in Spring Boot 3.x:

src/main/java/com/digitaldrift/fileupload/
├── FileUploadApplication.java
├── config/
│   └── FileStorageProperties.java
├── controller/
│   └── FileController.java
├── dto/
│   └── FileUploadResponse.java
├── entity/
│   └── FileMetadata.java
├── exception/
│   ├── FileStorageException.java
│   ├── MyFileNotFoundException.java
│   └── GlobalExceptionHandler.java
├── repository/
│   └── FileMetadataRepository.java
└── service/
    └── FileStorageService.java

5. Configure File Upload Properties

Configure custom upload limits and the target storage location in src/main/resources/application.yml:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/file_db
    username: postgres
    password: postgres_password
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

  # Configure Spring Boot Servlet Multipart limits
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 50MB

# Custom application properties for file storage
file:
  upload-dir: ./uploads

Configuration Class

Create a strongly-typed Java 21 configuration bean using @ConfigurationProperties:

package com.digitaldrift.fileupload.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "file")
public class FileStorageProperties {
    private String uploadDir;

    public String getUploadDir() {
        return uploadDir;
    }

    public void setUploadDir(String uploadDir) {
        this.uploadDir = uploadDir;
    }
}

6. Create FileUploadResponse DTO

Using Java 21 record syntax for lightweight, immutable Data Transfer Objects:

package com.digitaldrift.fileupload.dto;

public record FileUploadResponse(
    String fileId,
    String fileName,
    String fileDownloadUri,
    String fileType,
    long size
) {}

15. Store File Metadata in PostgreSQL

Before implementing the storage service, let's create the PostgreSQL JPA entity, repository, and exception classes to keep metadata trackable.

JPA Entity (`FileMetadata.java`)

package com.digitaldrift.fileupload.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "file_metadata")
public class FileMetadata {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String storedFileName;

    @Column(nullable = false)
    private String contentType;

    @Column(nullable = false)
    private Long fileSize;

    @Column(nullable = false)
    private String filePath;

    @Column(nullable = false)
    private LocalDateTime uploadedAt;

    public FileMetadata() {}

    public FileMetadata(String fileName, String storedFileName, String contentType, Long fileSize, String filePath) {
        this.fileName = fileName;
        this.storedFileName = storedFileName;
        this.contentType = contentType;
        this.fileSize = fileSize;
        this.filePath = filePath;
        this.uploadedAt = LocalDateTime.now();
    }

    // Getters and Setters
    public String getId() { return id; }
    public String getFileName() { return fileName; }
    public void setFileName(String fileName) { this.fileName = fileName; }
    public String getStoredFileName() { return storedFileName; }
    public void setStoredFileName(String storedFileName) { this.storedFileName = storedFileName; }
    public String getContentType() { return contentType; }
    public void setContentType(String contentType) { this.contentType = contentType; }
    public Long getFileSize() { return fileSize; }
    public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
    public String getFilePath() { return filePath; }
    public void setFilePath(String filePath) { this.filePath = filePath; }
    public LocalDateTime getUploadedAt() { return uploadedAt; }
    public void setUploadedAt(LocalDateTime uploadedAt) { this.uploadedAt = uploadedAt; }
}

Repository Interface (`FileMetadataRepository.java`)

package com.digitaldrift.fileupload.repository;

import com.digitaldrift.fileupload.entity.FileMetadata;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface FileMetadataRepository extends JpaRepository<FileMetadata, String> {
    Optional<FileMetadata> findByStoredFileName(String storedFileName);
}

Custom Exception Classes

package com.digitaldrift.fileupload.exception;

public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }
    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}

package com.digitaldrift.fileupload.exception;

public class MyFileNotFoundException extends RuntimeException {
    public MyFileNotFoundException(String message) {
        super(message);
    }
}

7. Create File Storage Service

The service handles disk I/O, file validation, UUID renaming for security, and database persistence:

package com.digitaldrift.fileupload.service;

import com.digitaldrift.fileupload.config.FileStorageProperties;
import com.digitaldrift.fileupload.entity.FileMetadata;
import com.digitaldrift.fileupload.exception.FileStorageException;
import com.digitaldrift.fileupload.exception.MyFileNotFoundException;
import com.digitaldrift.fileupload.repository.FileMetadataRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.*;
import java.util.*;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;
    private final FileMetadataRepository metadataRepository;

    private static final List<String> ALLOWED_CONTENT_TYPES = Arrays.asList(
        "image/jpeg", "image/png", "image/webp", "application/pdf", "text/plain", "application/zip"
    );

    @Autowired
    public FileStorageService(FileStorageProperties fileStorageProperties, FileMetadataRepository metadataRepository) {
        this.metadataRepository = metadataRepository;
        this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
                .toAbsolutePath().normalize();

        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new FileStorageException("Could not create the upload directory where files will be stored.", ex);
        }
    }

    public FileMetadata storeFile(MultipartFile file) {
        // 1. Validate empty file
        if (file.isEmpty()) {
            throw new FileStorageException("Failed to store empty file.");
        }

        // 2. Validate MIME type
        String contentType = file.getContentType();
        if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
            throw new FileStorageException("File type not allowed: " + contentType);
        }

        // 3. Clean and sanitize filename
        String originalFileName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
        if (originalFileName.contains("..")) {
            throw new FileStorageException("Filename contains invalid path sequence: " + originalFileName);
        }

        // 4. Generate unique stored filename to prevent collisions
        String fileExtension = "";
        int lastDotIndex = originalFileName.lastIndexOf('.');
        if (lastDotIndex > 0) {
            fileExtension = originalFileName.substring(lastDotIndex);
        }
        String storedFileName = UUID.randomUUID().toString() + fileExtension;

        try {
            // 5. Copy file to target location
            Path targetLocation = this.fileStorageLocation.resolve(storedFileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            // 6. Save metadata in PostgreSQL
            FileMetadata metadata = new FileMetadata(
                originalFileName,
                storedFileName,
                contentType,
                file.getSize(),
                targetLocation.toString()
            );

            return metadataRepository.save(metadata);

        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + originalFileName + ". Please try again!", ex);
        }
    }

    public Resource loadFileAsResource(String fileId) {
        FileMetadata metadata = metadataRepository.findById(fileId)
                .orElseThrow(() -> new MyFileNotFoundException("File not found with id " + fileId));

        try {
            Path filePath = this.fileStorageLocation.resolve(metadata.getStoredFileName()).normalize();
            Resource resource = new UrlResource(filePath.toUri());

            if (resource.exists() && resource.isReadable()) {
                return resource;
            } else {
                throw new MyFileNotFoundException("File not found or unreadable: " + metadata.getFileName());
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException("File path error for id " + fileId, ex);
        }
    }

    public FileMetadata getFileMetadata(String fileId) {
        return metadataRepository.findById(fileId)
                .orElseThrow(() -> new MyFileNotFoundException("File metadata not found with id " + fileId));
    }

    public List<FileMetadata> getAllFiles() {
        return metadataRepository.findAll();
    }

    public void deleteFile(String fileId) {
        FileMetadata metadata = getFileMetadata(fileId);
        try {
            Path filePath = this.fileStorageLocation.resolve(metadata.getStoredFileName()).normalize();
            Files.deleteIfExists(filePath);
            metadataRepository.delete(metadata);
        } catch (IOException ex) {
            throw new FileStorageException("Could not delete file with id " + fileId, ex);
        }
    }
}

8. Create Upload Controller

9. Upload Single File

10. Upload Multiple Files

11. Download File API

12. View Images in Browser

13. Delete Uploaded Files

16. REST API Endpoints

Here is the full @RestController implementation covering single uploads, batch uploads, file downloads (attachment header), inline image previews, deletion, and file listing:

package com.digitaldrift.fileupload.controller;

import com.digitaldrift.fileupload.dto.FileUploadResponse;
import com.digitaldrift.fileupload.entity.FileMetadata;
import com.digitaldrift.fileupload.service.FileStorageService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/files")
public class FileController {

    private final FileStorageService fileStorageService;

    @Autowired
    public FileController(FileStorageService fileStorageService) {
        this.fileStorageService = fileStorageService;
    }

    // 1. Upload Single File
    @PostMapping("/upload")
    public ResponseEntity<FileUploadResponse> uploadFile(@RequestParam("file") MultipartFile file) {
        FileMetadata metadata = fileStorageService.storeFile(file);

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/api/files/download/")
                .path(metadata.getId())
                .toUriString();

        FileUploadResponse response = new FileUploadResponse(
                metadata.getId(),
                metadata.getFileName(),
                fileDownloadUri,
                metadata.getContentType(),
                metadata.getFileSize()
        );

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    // 2. Upload Multiple Files
    @PostMapping("/upload-multiple")
    public ResponseEntity<List<FileUploadResponse>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        List<FileUploadResponse> responses = Arrays.stream(files)
                .map(this::uploadFile)
                .map(ResponseEntity::getBody)
                .collect(Collectors.toList());

        return ResponseEntity.status(HttpStatus.CREATED).body(responses);
    }

    // 3. Download File (Forces download dialog via Content-Disposition: attachment)
    @GetMapping("/download/{fileId}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
        FileMetadata metadata = fileStorageService.getFileMetadata(fileId);
        Resource resource = fileStorageService.loadFileAsResource(fileId);

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(metadata.getContentType()))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + metadata.getFileName() + "\"")
                .body(resource);
    }

    // 4. View Image / Document inline in Browser (Content-Disposition: inline)
    @GetMapping("/view/{fileId}")
    public ResponseEntity<Resource> viewFileInBrowser(@PathVariable String fileId, HttpServletRequest request) {
        FileMetadata metadata = fileStorageService.getFileMetadata(fileId);
        Resource resource = fileStorageService.loadFileAsResource(fileId);

        String contentType = metadata.getContentType();
        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + metadata.getFileName() + "\"")
                .body(resource);
    }

    // 5. List All File Metadata
    @GetMapping
    public ResponseEntity<List<FileMetadata>> getAllFiles() {
        List<FileMetadata> files = fileStorageService.getAllFiles();
        return ResponseEntity.ok(files);
    }

    // 6. Delete File
    @DeleteMapping("/{fileId}")
    public ResponseEntity<Void> deleteFile(@PathVariable String fileId) {
        fileStorageService.deleteFile(fileId);
        return ResponseEntity.noContent().build();
    }
}

14. Validate Uploaded Files

Proper file validation guards your server against disk exhaustion and malicious script injection:

  • Empty File Check: Verify file.isEmpty() before processing.
  • File Size Check: Enforced via Spring Boot properties spring.servlet.multipart.max-file-size.
  • MIME Type Validation: Restrict uploads strictly against an allowed white-list of content types.
  • Path Traversal Check: Sanitize names using StringUtils.cleanPath() and ensure no relative sequences (..) are passed.

17. JSON Responses

Single Upload Success Response (`201 Created`)

{
  "fileId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "fileName": "spring-architecture.png",
  "fileDownloadUri": "http://localhost:8080/api/files/download/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "fileType": "image/png",
  "size": 1048576
}

Multiple Upload Success Response (`201 Created`)

[
  {
    "fileId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "fileName": "document1.pdf",
    "fileDownloadUri": "http://localhost:8080/api/files/download/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "fileType": "application/pdf",
    "size": 204800
  },
  {
    "fileId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "fileName": "banner.webp",
    "fileDownloadUri": "http://localhost:8080/api/files/download/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "fileType": "image/webp",
    "size": 512000
  }
]

18. Testing with Postman

  1. Open Postman and create a new request.
  2. Set HTTP method to POST and URL to http://localhost:8080/api/files/upload.
  3. Select the Body tab, choose form-data.
  4. In the key column, type file and switch key type from Text to File.
  5. In the value column, click select files and choose an image or PDF from your computer.
  6. Click Send. You should receive a 201 Created response with the file metadata JSON.

19. cURL Examples

Upload Single File

curl -X POST http://localhost:8080/api/files/upload \
  -F "file=@/path/to/my-image.png"

Upload Multiple Files

curl -X POST http://localhost:8080/api/files/upload-multiple \
  -F "files=@/path/to/doc1.pdf" \
  -F "files=@/path/to/doc2.pdf"

Download File

curl -O -J http://localhost:8080/api/files/download/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

Delete File

curl -X DELETE http://localhost:8080/api/files/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

20. Common Errors & Exception Handling

Global Exception Handler (`GlobalExceptionHandler.java`)

Handle MaxUploadSizeExceededException and custom file exceptions centrally:

package com.digitaldrift.fileupload.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<Map<String, Object>> handleMaxSizeException(MaxUploadSizeExceededException exc) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.EXPECTATION_FAILED.value());
        body.put("error", "Expectation Failed");
        body.put("message", "File size exceeds configured maximum limit (10MB)");

        return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(body);
    }

    @ExceptionHandler(MyFileNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleFileNotFoundException(MyFileNotFoundException exc) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.NOT_FOUND.value());
        body.put("error", "Not Found");
        body.put("message", exc.getMessage());

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(FileStorageException.class)
    public ResponseEntity<Map<String, Object>> handleFileStorageException(FileStorageException exc) {
        Map<String, Object> body = new HashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("error", "Bad Request");
        body.put("message", exc.getMessage());

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
    }
}

21. Security Best Practices

  1. Rename Files to UUIDs: Never store files using the client's original file name on disk. A filename like ../../../etc/passwd or shell.jsp can overwrite critical files or trigger remote code execution.
  2. Validate MIME Type Server-Side: Inspect Content-Type headers and Apache Tika magic bytes to verify true file formats rather than trusting client extensions.
  3. Prevent Path Traversal: Always sanitize filename strings using StringUtils.cleanPath() and ensure resolved paths lie strictly inside your configured base upload directory.
  4. Restrict Executable Files: Disable execution permissions on the upload folder at the operating system level (e.g., chmod -R 644 uploads/).
  5. Virus & Malware Scanning: Integrate anti-virus solutions (such as ClamAV) to scan uploaded files before making them accessible to users.

22. Production Considerations

  • Store Files Outside Application Directory: Never save uploaded files inside the compiled JAR/WAR directory or embedded Tomcat static resources. Use external volumes or dedicated storage paths.
  • Use Cloud Object Storage: For containerized deployments (Kubernetes, AWS ECS, Docker), upload files to Amazon S3, Google Cloud Storage, or Azure Blob Storage instead of local disk.
  • CDN Integration: Route static media through a Content Delivery Network (Cloudflare, AWS CloudFront) for high-performance global caching and reduced server load.
  • Backup & Retention Strategy: Implement automated nightly snapshots for persistent disk volumes and database metadata tables.

23. Frequently Asked Questions

Q1: Why does Spring Boot throw MaxUploadSizeExceededException?
This occurs when an uploaded file or total multipart request size exceeds the thresholds defined in spring.servlet.multipart.max-file-size and spring.servlet.multipart.max-request-size.

Q2: Is storing binary BLOBs inside PostgreSQL a bad practice?
For small files under 256KB (like thumbnails), BLOBs work fine. However, storing large files in PostgreSQL inflates database size, slows down backups, and increases memory consumption. Storing file binaries on disk/cloud storage and metadata in PostgreSQL is the industry standard.

24. Interview Questions & Answers

Q1: How does Content-Disposition: inline differ from Content-Disposition: attachment?
inline instructs the web browser to render the file directly inside the browser tab (ideal for images and PDFs), whereas attachment forces the browser to prompt a file save download dialog.

Q2: How do you prevent Path Traversal vulnerabilities during file uploads in Java?
Sanitize input filenames, strip relative path sequences (..), resolve paths against a trusted root directory, and verify that the target path starts with the normalized root directory path.

25. Conclusion

Building a robust spring boot file upload and download architecture requires attention to file validation, memory management, database metadata mapping, and security guardrails. By combining Spring Boot 3.x, Java 21 records, PostgreSQL metadata tracking, and clean exception handling, you have built a scalable, production-ready foundation.

Next steps: Connect your storage service to Amazon S3 using the AWS SDK v2, add Spring Boot JWT Authentication to protect upload endpoints, and implement pagination using our Spring Boot Pagination and Sorting Guide.