Spring Boot File Upload & Download REST API — Store Files in MySQL/Oracle

Spring Boot File Upload to Database and Download REST API (MySQL & Oracle)

Handling file uploads is a foundational requirement for modern backend systems. Whether you are storing user profile pictures, PDF reports, or CSV data dumps, you need a secure and efficient way to transfer these files from a client to your database.

In this step-by-step guide, we will build a production-ready Spring Boot 3 REST API that accepts file uploads via MultipartFile, securely stores them as BLOBs (Binary Large Objects) in MySQL or Oracle, and exposes endpoints to list and download them.

📺 Visual Learner? Watch the Video Walkthrough!
Want to see this REST API built from scratch, tested with Postman, and connected to a database live?

▶ Subscribe to Spring Java Lab on YouTube

1. Database vs. File System: Where to store files?

  • Store in DB (BLOB): Best for strict ACID compliance, when files are moderate in size (under 10MB), and when you want your file backups to be perfectly synchronized with your relational data.
  • Store on File System / Object Storage (S3): Best for large files (videos, high-res images), streaming requirements, or when utilizing a Content Delivery Network (CDN) to reduce database storage costs.

2. Project Setup & Maven Dependencies

Generate a Spring Boot 3.x project using Spring Initializr. We need Spring Web, Spring Data JPA, and the appropriate database driver (MySQL or Oracle).

<dependencies>
  <!-- Web & REST -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- Spring Data JPA -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>

  <!-- MySQL Driver (Primary Example) -->
  <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
  </dependency>

  <!-- OR: Oracle JDBC driver (if you use Oracle) -->
  <!--
  <dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc11</artifactId>
    <scope>runtime</scope>
  </dependency>
  -->

  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

3. Application Configuration (Preventing OOM)

Configure your application.properties to connect to your database. More importantly, we must configure Spring Boot's internal multipart limits to protect the server's memory.

🚨 Production Gotcha: Preventing OOM Errors
By default, Spring Boot limits file uploads to 1MB. You must specifically tune max-file-size and max-request-size to allow larger files, but keep the limit reasonable to prevent attackers from crashing your JVM memory by uploading 5GB files!
# --- Database Configuration (MySQL) ---
spring.datasource.url=jdbc:mysql://localhost:3306/file_storage?useSSL=false
spring.datasource.username=root
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update

# --- Multipart File Constraints ---
# Maximum size for a single file
spring.servlet.multipart.max-file-size=10MB
# Maximum size for the entire multipart request (multiple files)
spring.servlet.multipart.max-request-size=15MB

4. Designing the File Entity (@Lob)

We use the @Lob annotation to tell Hibernate to map the byte[] array to a Large Object database type. In MySQL, this translates to LONGBLOB, and in Oracle, it becomes a BLOB.

package com.example.filestorage.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "stored_file")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FileEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(nullable = false)
  private String fileName;

  @Column(nullable = false)
  private String contentType;

  @Column(nullable = false)
  private Long size;

  @Lob
  @Column(nullable = false, length = 10485760) // Optional: Specify length for DB optimization
  private byte[] data;
}

5. Repository Layer

We create a simple JpaRepository interface. Spring Data JPA will automatically implement the standard CRUD queries for our FileEntity.

package com.example.filestorage.repository;

import com.example.filestorage.entity.FileEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FileRepository extends JpaRepository<FileEntity, Long> {
}

6. Service Layer (Upload & Download Logic)

The Service layer converts the incoming MultipartFile into our FileEntity. Notice the @Transactional annotations. Reading and writing Large Objects (LOBs) often requires an active database transaction.

package com.example.filestorage.service;

import com.example.filestorage.entity.FileEntity;
import com.example.filestorage.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {

  private final FileRepository fileRepository;

  @Transactional
  public FileEntity storeFile(MultipartFile file) {
    try {
      log.info("Saving file: {} (Size: {} bytes)", file.getOriginalFilename(), file.getSize());
      
      FileEntity entity = FileEntity.builder()
          .fileName(file.getOriginalFilename())
          .contentType(file.getContentType())
          .size(file.getSize())
          // NOTE: getBytes() loads the entire file into RAM. Ensure max-file-size is configured!
          .data(file.getBytes()) 
          .build();

      return fileRepository.save(entity);
    } catch (IOException ex) {
      log.error("Failed to store file", ex);
      throw new RuntimeException("Could not store file: " + file.getOriginalFilename(), ex);
    }
  }

  @Transactional(readOnly = true)
  public FileEntity getFile(Long id) {
    return fileRepository.findById(id)
        .orElseThrow(() -> new RuntimeException("File not found with id: " + id));
  }

  @Transactional(readOnly = true)
  public List<FileEntity> listFiles() {
    return fileRepository.findAll();
  }
}

7. DTOs for API Responses

When listing files, we don't want to send the raw binary byte[] data over the network for every file. We create a FileResponse DTO to expose only the metadata.

package com.example.filestorage.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FileResponse {
  private Long id;
  private String fileName;
  private String contentType;
  private Long size;
}

8. REST Controller (Upload, List, Download)

Our controller handles the HTTP mapping. For the download endpoint, we utilize HttpHeaders.CONTENT_DISPOSITION. Setting this to "attachment; filename=..." forces the browser to download the file to the user's hard drive.

package com.example.filestorage.controller;

import com.example.filestorage.dto.FileResponse;
import com.example.filestorage.entity.FileEntity;
import com.example.filestorage.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ByteArrayResource;
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.util.List;
import java.util.stream.Collectors;

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

  private final FileService fileService;

  @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
  public ResponseEntity<FileResponse> uploadFile(@RequestPart("file") MultipartFile file) {
    FileEntity saved = fileService.storeFile(file);

    FileResponse response = FileResponse.builder()
        .id(saved.getId())
        .fileName(saved.getFileName())
        .contentType(saved.getContentType())
        .size(saved.getSize())
        .build();

    return ResponseEntity.ok(response);
  }

  @GetMapping
  public ResponseEntity<List<FileResponse>> listFiles() {
    List<FileResponse> files = fileService.listFiles().stream()
        .map(entity -> FileResponse.builder()
            .id(entity.getId())
            .fileName(entity.getFileName())
            .contentType(entity.getContentType())
            .size(entity.getSize())
            .build())
        .collect(Collectors.toList());

    return ResponseEntity.ok(files);
  }

  @GetMapping("/{id}")
  public ResponseEntity<ByteArrayResource> downloadFile(@PathVariable Long id) {
    FileEntity entity = fileService.getFile(id);

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

9. Testing the API with cURL

Upload a file

# Linux/macOS
curl -X POST "http://localhost:8080/api/files" \
  -H "Content-Type: multipart/form-data" \
  -F "file=@/tmp/invoice.pdf"

# Windows Command Prompt
curl -X POST "http://localhost:8080/api/files" ^
  -H "Content-Type: multipart/form-data" ^
  -F "file=@C:/temp/invoice.pdf"

List files

curl http://localhost:8080/api/files

Download a file (Save to Disk)

curl -X GET "http://localhost:8080/api/files/1" -o downloaded_invoice.pdf

10. Common Pitfalls and How to Avoid Them

ProblemCauseFix
413 Payload Too Large File size exceeds Tomcat/Spring Boot limits. Configure spring.servlet.multipart.max-file-size and max-request-size.
Could not store file exception Database column too small for BLOB data. Ensure you use a proper BLOB/LONGBLOB column. Let JPA generate the schema correctly or tune your DDL.
OutOfMemoryError Huge files stored as BLOBs loaded entirely into RAM. Set reasonable file size limits in properties. For massive files, use Object Storage (S3) instead of a Database.

📁 Build Secure File APIs & Architecture

Implementing file uploads requires careful handling of HTTP multipart requests, database storage tuning, and security constraints. Explore these guides to master enterprise REST architectures.

📘 Spring Boot JPA Basics

Understand the entities, repositories, and ORM mappings used to store metadata.

🧩 Spring Boot CRUD API (Oracle DB)

See how file upload/download concepts integrate into real-world CRUD APIs.

📄 Pagination & Sorting

Learn how to query and list thousands of uploaded files efficiently without crashing memory.

🗄️ Database Performance Tuning

Learn the performance impact and indexing strategies when storing BLOBs in MySQL or Oracle.

🚨 Global Exception Handling

Handle MaxUploadSizeExceededException, invalid formats, and upload errors gracefully using @RestControllerAdvice.

💼 Spring Boot Interview Questions (2–5 Years)

Real-world interview discussions surrounding file handling, transactions, and REST bottlenecks.