DEV Community

Galisetty Priyatham
Galisetty Priyatham

Posted on

Building a Mailbox Pattern in Spring Boot to Decouple Your Application Layers

Introduction
In a typical Spring Boot application, synchronous service calls between layers can create tight coupling and reduce flexibility for long-running or delayed processes. In this article, we’ll walk through a clean implementation of the Mailbox Pattern using BlockingQueue, allowing decoupled, asynchronous processing between the controller and the background worker.

Imagine how you drop a letter in a mailbox and the postman picks it up later. You don’t wait at the box for delivery to happen.

Similarly, the Mailbox Pattern in a Spring application allows you to accept incoming tasks instantly, drop them into a queue, and let a background thread process them later. This makes your application faster, more scalable, and loosely coupled.

Use Case
Imagine you’re receiving task requests via a REST API, but you don’t want to process them immediately either because they’re slow or you want to manage them in batches or queues.

A Mailbox Pattern helps solve this by:

  • 📨 Receiving the task
  • 📥 Putting it into a mailbox (queue)
  • 🛠️ Processing it in a background worker

Architecture

+------------+        +-------------+        +-------------+
| REST API   |        | Mailbox     |        | Worker      |
| Controller | -----> | (Queue)     | -----> | Thread/Task |
+------------+        +-------------+        +-------------+
Enter fullscreen mode Exit fullscreen mode

Tech Stack

  • Spring Boot 3
  • Java 17+
  • Maven

Project Structure

spring-mailbox-demo/
├── pom.xml
└── src/
    └── main/
        ├── java/com/example/mailbox/
        │   ├── SpringMailboxDemoApplication.java
        │   ├── controller/TaskController.java
        │   ├── model/TaskRequest.java
        │   └── service/
        │       ├── Mailbox.java
        │       └── TaskWorker.java
        └── resources/application.properties

Enter fullscreen mode Exit fullscreen mode

Full Source Code

<project xmlns="http://maven.apache.org/POM/4.0.0"
         ...>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-mailbox-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.2</version>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Enter fullscreen mode Exit fullscreen mode

SpringMailboxDemoApplication.java

package com.example.mailbox;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringMailboxDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringMailboxDemoApplication.class, args);
    }
}

Enter fullscreen mode Exit fullscreen mode

model/TaskRequest.java

package com.example.mailbox.model;

public class TaskRequest {
    private String taskId;
    private String payload;

    public TaskRequest() {}

    public TaskRequest(String taskId, String payload) {
        this.taskId = taskId;
        this.payload = payload;
    }

    public String getTaskId() {
        return taskId;
    }

    public void setTaskId(String taskId) {
        this.taskId = taskId;
    }

    public String getPayload() {
        return payload;
    }

    public void setPayload(String payload) {
        this.payload = payload;
    }
}

Enter fullscreen mode Exit fullscreen mode

How does the Controller fit in?
The controller is like the person dropping a letter in the mailbox. It doesn't care how or when the letter gets delivered — it just drops it and responds immediately with “Task submitted.”

TaskController.java

@RestController
@RequestMapping("/task")
public class TaskController {

    @Autowired
    private Mailbox mailbox;

    @PostMapping
    public ResponseEntity<String> submitTask(@RequestBody TaskRequest task) {
        mailbox.submit(task); // drop it into the mailbox
        return ResponseEntity.ok("Task submitted: " + task.getTaskId());
    }
}

Enter fullscreen mode Exit fullscreen mode

📥 What is the Mailbox really doing?

  • We use a BlockingQueue in Java, which works like a thread-safe to-do list.
  • The controller adds a task to it using offer().
  • The worker thread pulls tasks from it using take().
  • This lets the web request return instantly while the real work is still happening behind the scenes.

service/Mailbox.java

package com.example.mailbox.service;

import com.example.mailbox.model.TaskRequest;
import org.springframework.stereotype.Component;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

@Component
public class Mailbox {
    private final BlockingQueue<TaskRequest> queue = new LinkedBlockingQueue<>();

    public void submit(TaskRequest request) {
        queue.offer(request);
    }

    public TaskRequest take() throws InterruptedException {
        return queue.take();
    }
}

Enter fullscreen mode Exit fullscreen mode

What is the Worker Thread doing?
The worker acts like a postman. It runs in the background and keeps checking the mailbox for new tasks.
When it finds one, it processes it (in our case, just logs it). You can later expand this to send emails, call APIs, or save to a DB.

service/TaskWorker.java

package com.example.mailbox.service;

import com.example.mailbox.model.TaskRequest;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TaskWorker {

    @Autowired
    private Mailbox mailbox;

    @PostConstruct
    public void startWorker() {
        Thread workerThread = new Thread(() -> {
            while (true) {
                try {
                    TaskRequest task = mailbox.take();
                    System.out.println("Processing task: " + task.getTaskId() + " | Payload: " + task.getPayload());
                    Thread.sleep(2000); // simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });

        workerThread.setDaemon(true);
        workerThread.start();
    }
}

Enter fullscreen mode Exit fullscreen mode

application.properties

server.port=8080

Enter fullscreen mode Exit fullscreen mode

Why It Matters

  • Helps with performance by not blocking HTTP threads.
  • Supports scalability by isolating heavy processing.
  • Promotes loose coupling between controller and business logic.

You can expand this pattern by:

  • Adding retry or error queues
  • Using ScheduledExecutorService
  • Swapping BlockingQueue with a Kafka topic or Spring Integration MessageChannel

Even if you're a beginner, this pattern teaches you:

  • How to run background threads in Spring
  • How to decouple HTTP input from business logic
  • How to simulate real-world job queues
  • You now understand not just how to build it but why it’s useful in real-world applications.

Summary
This pattern may seem simple, but it unlocks powerful asynchronous processing patterns for real-world Spring applications with minimal complexity and maximum decoupling.

You can also download from :

GitHub logo pgalisetty / spring-mailbox-demo

A Mailbox Pattern example

Top comments (0)