Bulkhead in Spring Boot Microservices Explained Like You’re a Dog πΆ
SEO Title: Bulkhead Pattern in Spring Boot Microservices Using Resilience4j
Meta Description: Learn the Bulkhead pattern in Spring Boot microservices with simple dog-friendly examples. Understand Semaphore Bulkhead, Thread Pool Bulkhead, configuration, and Java code using Resilience4j.
Tags: Java, Spring Boot, Microservices, Bulkhead Pattern, Resilience4j, Fault Tolerance, Semaphore, Thread Pool
1. Imagine You’re a Dog Living in a Big House π
Hey, buddy! Imagine you are a dog living in a big house.
Your house has three important rooms:
- A food room where you get biscuits.
- A playroom where you play with your toys.
- A sleeping room where you sleep peacefully.
Now imagine something terrible happens.
All the dogs in your neighborhood suddenly enter the food room. They start eating, barking, running, and fighting for biscuits.
Soon, the food room becomes overcrowded.
But that’s not the biggest problem.
The dogs start blocking the entrance to the other rooms, too. Now nobody can reach the playroom or the sleeping room.
Even dogs that just want to sleep peacefully cannot enter their room!
Your owner comes up with a clever solution.
Your owner creates separate spaces and limits how many dogs can enter each space.
Now, even if the food room becomes overcrowded, the sleeping room remains available.
Congratulations! πΆ
You have just understood the basic idea behind the Bulkhead Pattern.
In microservices, a Bulkhead helps isolate resources so that problems in one operation or dependency do not consume all the resources needed by other operations.
Let’s understand how this works in Java and Spring Boot.
2. What Is the Bulkhead Pattern?
Imagine your house is an application, and each room represents a different operation.
In a Spring Boot microservices application, an Order Service might call multiple downstream services:
- Product Service
- Payment Service
- Inventory Service
Suppose the Payment Service becomes very slow.
If calls to Payment Service occupy all the available threads in Order Service, requests to Product Service may also stop working properly.
The Bulkhead Pattern helps prevent this situation by isolating resources or limiting concurrency.
Think of it as building walls between the rooms of your house.
Order Service
|
+--------+--------+
| |
v v
Product API Payment API
| |
Product Service Payment Service
(Fast) (Slow)
Separate resource limits help
prevent one operation from
consuming all shared resources.
The main goal: A slow or overloaded downstream operation should not bring down unrelated operations because they share the same limited resources.
3. RateLimiter vs. Bulkhead: What’s the Difference?
Before we go further, let’s remember the previous article about RateLimiter.
Imagine your owner gives you two rules.
Rule 1: You can ask for biscuits only 5 times every 10 seconds.
This is similar to a RateLimiter.
Rule 2: Only 3 dogs can enter the food room at the same time.
This is similar to a Bulkhead.
The difference is important.
| Feature | RateLimiter | Bulkhead |
|---|---|---|
| Main purpose | Control request frequency | Limit concurrent operations or isolate resources |
| Main question | How many requests can be admitted during a period? | How many operations can run concurrently? |
| Example | 5 requests every 10 seconds | 3 concurrent calls |
| Helps prevent | Excessive request traffic | Resource exhaustion caused by too much concurrency |
A RateLimiter controls the rate of requests. A Bulkhead controls concurrency or resource isolation.
You can use both together when your application needs both protections.
4. Why Do We Need Bulkhead in Microservices?
Let’s imagine your Order Service has two APIs.
- API 1 calls Product Service.
- API 2 calls Payment Service.
The Product Service is fast and takes around 100 milliseconds to process a request.
The Payment Service is slow and takes around 5 seconds.
Now imagine many requests arrive at API 2.
Each request calls Payment Service, and each call occupies a thread while waiting for the response.
If enough requests arrive, all the available Order Service threads might become occupied by Payment API calls.
Now a customer tries to view product details.
Unfortunately, API 1 cannot get an available thread, even though Product Service is working perfectly.
The Payment Service has indirectly affected the Product API.
This is a resource-isolation problem.
A Bulkhead can help protect the application by limiting the resources allocated to the Payment operation or by placing that operation in a dedicated thread pool.
Let’s explore the two types of Bulkhead provided by Resilience4j.
5. Types of Bulkhead in Resilience4j
Resilience4j provides two main Bulkhead implementations:
- Semaphore Bulkhead
- Thread Pool Bulkhead
They solve related problems but work differently.
5.1 Semaphore Bulkhead
Imagine your owner creates a food room with exactly three entry passes.
Only three dogs can enter the room at a time.
When one dog leaves, its entry pass becomes available for another dog.
If three dogs are already inside, another dog cannot enter immediately. Depending on the configured waiting duration, it may have to wait briefly or be rejected.
This is similar to a Semaphore Bulkhead.
A Semaphore Bulkhead limits the number of concurrent executions using permits.
For example:
Order Service
|
v
Semaphore Bulkhead
|
+---- Request 1 ----> Product Service
|
+---- Request 2 ----> Product Service
|
+---- Request 3 ----> Product Service
|
+---- Request 4 ----> Rejected
Suppose the maximum number of concurrent calls is 3.
- Request 1 acquires a permit.
- Request 2 acquires a permit.
- Request 3 acquires a permit.
- Request 4 cannot acquire a permit while all three are occupied.
When a running call completes, its permit is released.
Important: A Semaphore Bulkhead limits concurrent executions. It does not create a dedicated thread pool for the protected operation.
5.2 Thread Pool Bulkhead
Now imagine your house has separate rooms with their own staff.
The food room has its own team of workers, and the playroom has a different team.
If the food room becomes extremely busy, its workers cannot take over all the workers assigned to the playroom.
In software, a Thread Pool Bulkhead provides a dedicated thread pool and queue for the protected operation.
For example:
Order Service
|
+---------+---------+
| |
v v
Product Bulkhead Payment Bulkhead
| |
Dedicated Pool Dedicated Pool
| |
v v
Product Service Payment Service
If Payment Service becomes slow, the Payment Bulkhead’s threads and queue can become busy without directly consuming the dedicated thread pool configured for Product Service.
This provides stronger isolation between those operations.
Important: Thread Pool Bulkhead uses asynchronous execution. It is commonly used with methods returning CompletableFuture and is also useful for appropriate asynchronous workflows.
6. Implement Semaphore Bulkhead in Spring Boot
Now let’s write some Java code.
Imagine we have an Order Service calling a Product Service through a Feign client.
We want to allow only two concurrent calls to Product Service.
Step 1: Add the Resilience4j dependency
Add this dependency to your Maven pom.xml:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.1.0</version>
</dependency>
Use a version compatible with your Spring Boot project.
Step 2: Create the Product Feign Client
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/products/{id}")
String getProductById(
@PathVariable("id") String id
);
}
This client allows Order Service to request product details from Product Service.
Step 3: Apply the Semaphore Bulkhead
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final ProductClient productClient;
public OrderService(ProductClient productClient) {
this.productClient = productClient;
}
@Bulkhead(
name = "productService",
type = Bulkhead.Type.SEMAPHORE,
fallbackMethod = "productFallback"
)
public String invokeProductAPI(String id) {
return productClient.getProductById(id);
}
public String productFallback(
String id,
Throwable t) {
return "Product Service is busy. Please try again later.";
}
}
Let’s understand the annotation.
@Bulkhead
This enables Bulkhead protection for the method.
name = "productService"
This identifies the Bulkhead configuration to use.
type = Bulkhead.Type.SEMAPHORE
This tells Resilience4j to use the Semaphore implementation.
fallbackMethod = "productFallback"
This provides an alternative response when a failure is handled by the configured fallback mechanism.
The fallback method should have a compatible return type and the original parameters, optionally followed by a compatible exception parameter.
Step 4: Configure the Semaphore Bulkhead
Add these properties to application.properties:
server.port=8081
spring.application.name=order-service
resilience4j.bulkhead.instances.productService.maxConcurrentCalls=2
resilience4j.bulkhead.instances.productService.maxWaitDuration=0
Let’s understand them.
| Property | Meaning |
|---|---|
maxConcurrentCalls=2 |
Allow up to two concurrent executions |
maxWaitDuration=0 |
Do not wait for a permit when all permits are occupied |
The result is simple.
Request 1 -> Accepted
Request 2 -> Accepted
Request 3 -> Rejected while both permits are occupied
The third request can be accepted later when a permit becomes available, assuming it is retried or sent again.
Step 5: Create the Controller
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public String getOrder(@PathVariable String id) {
return orderService.invokeProductAPI(id);
}
}
Now you can test the endpoint:
GET http://localhost:8081/orders/101
Send several requests concurrently to test the Bulkhead behavior. Sending requests one after another may not exceed the concurrency limit if each request completes quickly.
7. Understanding maxWaitDuration
What if your owner says:
“If the food room is full, you can wait a little before giving up.”
That is the idea behind maxWaitDuration.
For a Semaphore Bulkhead, it controls how long a caller waits to acquire a permit when no permit is available.
Examples:
# Reject immediately
resilience4j.bulkhead.instances.productService.maxWaitDuration=0
# Wait up to 300 milliseconds
resilience4j.bulkhead.instances.productService.maxWaitDuration=300ms
# Wait up to 2 seconds
resilience4j.bulkhead.instances.productService.maxWaitDuration=2s
Choose a waiting duration carefully.
Waiting too long can increase response latency and tie up caller resources. Rejecting immediately may be preferable when the application should fail fast under overload.
8. Implement Thread Pool Bulkhead in Spring Boot
Now let’s build the second type.
Imagine your owner creates a dedicated room with three workers and space for two dogs waiting in a queue.
Only those workers handle tasks for that room.
When all three workers are busy, up to two more tasks can wait in the queue. Additional tasks are rejected when the queue is full and no worker becomes available in time.
Let’s implement this using Resilience4j.
Step 1: Create the Order Service
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import java.util.concurrent.CompletableFuture;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final ProductClient productClient;
public OrderService(ProductClient productClient) {
this.productClient = productClient;
}
@Bulkhead(
name = "productService",
type = Bulkhead.Type.THREADPOOL,
fallbackMethod = "productFallback"
)
public CompletableFuture<String> invokeProductAPI(String id) {
return CompletableFuture.completedFuture(
productClient.getProductById(id)
);
}
public CompletableFuture<String> productFallback(
String id,
Throwable t) {
return CompletableFuture.completedFuture(
"Product Service is busy. Please try again later."
);
}
}
Let’s understand the important details.
Bulkhead.Type.THREADPOOL
This tells Resilience4j to use the Thread Pool Bulkhead.
CompletableFuture<String>
The method returns a CompletableFuture, which represents a result that can become available later.
The Bulkhead proxy
With annotation-based Thread Pool Bulkhead, Resilience4j submits the protected method execution to the configured bulkhead executor.
The CompletableFuture.completedFuture(...) inside the method simply wraps the result in a completed future. It does not itself move the work to another thread. The thread-pool behavior comes from the Bulkhead interception.
Because the Feign call is blocking, the dedicated bulkhead worker may remain occupied while waiting for Product Service. That is one reason the thread pool and downstream timeouts must be configured carefully.
Step 2: Configure the Thread Pool
Add these properties:
server.port=8081
spring.application.name=order-service
resilience4j.thread-pool-bulkhead.instances.productService.coreThreadPoolSize=3
resilience4j.thread-pool-bulkhead.instances.productService.maxThreadPoolSize=3
resilience4j.thread-pool-bulkhead.instances.productService.queueCapacity=2
Let’s understand them.
| Property | Meaning |
|---|---|
coreThreadPoolSize=3 |
Configure three core worker threads |
maxThreadPoolSize=3 |
Set the maximum worker thread count to three |
queueCapacity=2 |
Allow two tasks to wait in the queue |
In this configuration, the pool can execute three tasks concurrently and queue two additional tasks.
Assuming all three workers are busy and both queue positions are occupied, the next task can be rejected.
Thread Pool = 3 workers
Queue Size = 2
Request 1 -> Worker 1
Request 2 -> Worker 2
Request 3 -> Worker 3
Request 4 -> Queue
Request 5 -> Queue
Request 6 -> Rejected if capacity is still full
This is a simplified view of the pool under load. Actual results depend on execution timing and how quickly requests complete.
Step 3: Understand the Output
Suppose Product Service is slow, so all three worker threads remain occupied.
The queue already contains two waiting requests.
Another request arrives.
The Bulkhead cannot accept more work while the worker pool and queue are full, so the request can be rejected. The fallback can provide an alternative response if the rejection is handled by the configured fallback mechanism.
This prevents the bulkhead’s work queue from growing without a limit.
9. Semaphore Bulkhead vs. Thread Pool Bulkhead
Now let’s compare the two.
| Feature | Semaphore Bulkhead | Thread Pool Bulkhead |
|---|---|---|
| Main mechanism | Semaphore permits | Dedicated thread pool and queue |
| Limits concurrency | Yes | Yes, through pool capacity |
| Dedicated thread pool | No | Yes |
| Queue for waiting tasks | No dedicated task queue | Yes, configurable queue |
| Waiting behavior | Configurable permit wait | Tasks can wait in the queue |
| Typical use | Limiting concurrent calls | Isolating asynchronous tasks and worker resources |
Which One Should You Use?
Use a Semaphore Bulkhead when you want to limit the number of concurrent executions without creating a dedicated thread pool.
Use a Thread Pool Bulkhead when you want a dedicated executor and a bounded queue for the protected operation.
Neither option automatically solves every downstream failure. You should also configure appropriate connection and read timeouts, handle errors correctly, and monitor the system.
10. Real-World Example: An E-Commerce Application
Imagine an online shopping website.
The Order Service communicates with several services:
- Product Service returns product details.
- Payment Service processes payments.
- Inventory Service checks stock.
Suppose Payment Service becomes very slow during a major sale.
Without resource isolation, payment-related work might consume shared threads and make unrelated operations slower or unavailable.
A Bulkhead can help isolate the resources assigned to each operation.
E-Commerce Application
|
Order Service
|
+----------------+----------------+
| | |
v v v
Product API Payment API Inventory API
| | |
Product Pool Payment Pool Inventory Pool
| | |
v v v
Product Service Payment Service Inventory Service
This diagram illustrates a possible architecture using dedicated pools. The actual design may instead use Semaphore Bulkheads or other resource-isolation techniques, depending on the application’s needs.
The important idea is to avoid allowing one slow operation to consume all the resources needed by unrelated operations.
11. Important Things to Remember
Before implementing Bulkhead in a real project, keep these points in mind:
- Configure concurrency limits according to the capacity of the downstream service.
- Keep thread pools and queues bounded.
- Configure connection and read timeouts for blocking HTTP calls.
- Choose a meaningful fallback response or propagate an appropriate error.
- Monitor rejected calls, active threads, queue usage, and downstream response times.
- Remember that in-memory Bulkhead limits generally apply to an individual application instance, not automatically to every instance combined.
A Bulkhead is a resource-isolation mechanism, not a replacement for good capacity planning or monitoring.
12. Key Takeaways πΆ
Let’s finish our dog lesson.
Imagine your house has separate rooms. If the food room becomes overcrowded, you don’t want the sleeping room to become unavailable.
That’s exactly the problem the Bulkhead Pattern helps solve in microservices.
Remember these five things:
- Bulkhead helps isolate resources and control concurrent work.
- Semaphore Bulkhead uses permits to limit concurrent executions.
- Thread Pool Bulkhead uses a dedicated worker pool and bounded queue.
- Resilience4j provides both implementations for Spring Boot applications.
- Proper configuration helps prevent one overloaded operation from consuming resources needed by other operations.
The easiest way to remember it:
A RateLimiter controls how often dogs can ask for biscuits.
A Bulkhead controls how many dogs can occupy a room at the same time.
Both help your microservices stay healthier under heavy traffic.
And now, buddy, you’re ready for the next lesson!
In the next article, we’ll explore TimeLimiter in Spring Boot Microservices, using the same simple dog-friendly approach.
Until then, keep coding and keep learning! πΆπ»