Bulkhead Pattern in Microservices
Building resilient microservices with watertight compartments. In a microservices system, one failing component should never sink the whole application. A ship uses bulkheads, watertight walls, so that a leak in one compartment does not flood the entire hull. Software architects use the Bulkhead pattern in the same way: to isolate failures and keep the rest of the system running.
This guide explains the pattern in simple terms, shows the problem it solves, and walks through working Java examples, from a plain-Java bulkhead to Resilience4j with Spring Boot, along with sizing, monitoring and best practices.
1. What Is the Bulkhead Pattern?
The Bulkhead pattern is a resilience pattern that prevents a failure in one part of a system from spreading to other parts. It does this by dividing the system into isolated compartments, each with its own limited resources and its own failure boundary.
The core idea is simple: do not put all your eggs in one basket. When resources are separated, a compartment that fails or becomes overloaded can only use up its own resources. The other compartments keep working normally, which improves the fault tolerance and availability of the whole system.
2. From Ships to Software
Imagine a ship’s hull as one big open space. A single hole would let water flood everything, and the ship would sink. Real ships are built with several watertight compartments separated by bulkheads. If one compartment is breached, the water stays there and the ship stays afloat.
Without bulkheads With bulkheads
+-------------------+ +-----+-----+-----+
|~~~~~~~~~~~~~~~~~~~| |~~~~~| | |
|~~~~ flooded ~~~~~~| |~~~~~| OK | OK |
+-------------------+ +-----+-----+-----+
ship sinks one compartment floods,
the ship floats
In software, the “water” is usually resource exhaustion: threads, database connections, CPU or memory being used up. A single slow or misbehaving dependency can consume all of a shared resource and bring down other, perfectly healthy features. The Bulkhead pattern builds walls that keep this exhaustion contained.
3. The Problem: Resource Exhaustion and Cascading Failures
Most services handle work with limited, shared resources. A web server has a fixed number of request threads; an application has a limited database connection pool. Problems start when one dependency becomes slow, which is often worse than being completely down:
- A downstream service starts responding slowly, for example in 10 seconds instead of 100 milliseconds.
- Every request that calls it holds a thread while waiting.
- New requests keep arriving, and soon all threads are waiting on the slow service.
- Requests for completely unrelated features, which do not even use that service, now have no threads left and fail too.
- Other services that call this one start waiting as well, and the failure cascades through the system.
A fast failure, such as “connection refused”, releases resources immediately. A slow dependency holds them, which is why bulkheads are so valuable: they cap how much any single dependency can take.
4. Types of Bulkhead Isolation
| Type | What is isolated | Example |
|---|---|---|
| Thread pool isolation | Separate thread pools for calls to different dependencies | 10 threads for inventory calls, 5 threads for recommendation calls |
| Semaphore (concurrency) isolation | A limit on how many calls to a dependency may run at the same time | At most 20 concurrent calls to the payment service |
| Connection pool isolation | Separate database or HTTP connection pools | A small pool for heavy reports, a larger one for customer orders |
| Instance and deployment isolation | Separate service instances, clusters or Kubernetes namespaces | Checkout runs on its own nodes, apart from batch jobs |
| Client or tenant isolation | Separate capacity for different customers or client types | Mobile app traffic and partner API traffic use different instances |
The first three work inside one service (fine-grained bulkheads). The last two work at the infrastructure level (coarse-grained bulkheads): a hardware failure or severe outage in one cluster does not affect services running in another.
5. A Practical Example: E-Commerce Application
Consider a simplified e-commerce application with three microservices:
- Order service: processes customer orders.
- Inventory service: checks and updates product stock.
- Recommendation service: suggests related products to users.
5.1 Scenario without bulkheads
The order service calls both the inventory service and the recommendation service, using one shared thread pool for all outgoing HTTP calls.
- During a flash sale, the inventory service becomes slow and starts timing out under the load.
- Threads waiting for inventory responses pile up until the shared pool is exhausted.
- When the order service now tries to call the recommendation service, there are no free threads left.
- The recommendation service is perfectly healthy, yet it becomes unreachable. The whole order flow is degraded.
5.2 Scenario with bulkheads
We refactor the order service to use two dedicated bulkheads:
- Bulkhead A: only for calls to the inventory service.
- Bulkhead B: only for calls to the recommendation service.
Each bulkhead has a fixed number of threads and a small, bounded waiting queue.
Order Service
+------------+------------+
| |
[ Bulkhead A ] [ Bulkhead B ]
10 threads 5 threads
(all busy!) (still free)
| |
v v
Inventory Service Recommendation Service
(slow) (healthy)
The result: during the same flash sale, the slow inventory service fills up Bulkhead A, but Bulkhead B is untouched. The order service can still call the recommendation service and show product suggestions. The failure is contained, and the system degrades gracefully instead of failing completely.
6. Building a Simple Bulkhead in Plain Java
To understand how bulkheads work, it helps to build a tiny one yourself. A semaphore holds a fixed number of permits; each call takes one permit and returns it when finished. When no permits are left, extra calls are rejected quickly instead of waiting forever:
package com.example.resilience;
import java.time.Duration;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class SimpleBulkhead {
private final String name;
private final Semaphore permits;
private final Duration maxWait;
public SimpleBulkhead(String name, int maxConcurrentCalls, Duration maxWait) {
this.name = name;
this.permits = new Semaphore(maxConcurrentCalls);
this.maxWait = maxWait;
}
public <T> T call(Supplier<T> task, Supplier<T> fallback) {
boolean acquired;
try {
// Wait a short time for a free slot, then give up
acquired = permits.tryAcquire(maxWait.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return fallback.get();
}
if (!acquired) {
System.out.println("Bulkhead '" + name + "' is full - using fallback");
return fallback.get();
}
try {
return task.get();
} finally {
permits.release(); // always give the slot back
}
}
}
SimpleBulkhead inventoryBulkhead =
new SimpleBulkhead("inventory", 10, Duration.ofMillis(100));
SimpleBulkhead recommendationBulkhead =
new SimpleBulkhead("recommendations", 5, Duration.ofMillis(50));
Stock stock = inventoryBulkhead.call(
() -> inventoryClient.getStock(productId),
() -> Stock.unknown(productId));
List<Product> suggestions = recommendationBulkhead.call(
() -> recommendationClient.forProduct(productId),
List::of); // no suggestions is better than no page
Thread pool isolation works in a similar way with a separate ExecutorService per dependency. Always use a bounded queue; Executors.newFixedThreadPool() uses an unbounded queue, which only moves the pile-up from threads to memory:
package com.example.resilience;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public final class BulkheadExecutors {
private BulkheadExecutors() {
}
public static ThreadPoolExecutor create(int threads, int queueCapacity) {
return new ThreadPoolExecutor(
threads, threads,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueCapacity), // bounded queue
new ThreadPoolExecutor.AbortPolicy()); // reject when full
}
}
This is a learning example. In real projects, use a proven library such as Resilience4j, which adds configuration, metrics and integration with other resilience patterns.
7. Semaphore Bulkhead with Resilience4j
Resilience4j is the standard resilience library for modern Java and Spring Boot applications. Its semaphore bulkhead limits how many calls to a dependency can run at the same time.
7.1 Dependencies
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<!-- The annotations work through Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Add the Resilience4j version explicitly, or import its BOM. Spring Cloud Circuit Breaker with Resilience4j is an alternative that brings the same features.
7.2 Configuration
resilience4j:
bulkhead:
instances:
inventory:
max-concurrent-calls: 10 # at most 10 calls at the same time
max-wait-duration: 100ms # wait briefly for a free slot, then reject
recommendations:
max-concurrent-calls: 5
max-wait-duration: 0 # reject immediately when full
7.3 Protecting a client with @Bulkhead
package com.example.order.client;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.bulkhead.BulkheadFullException;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
@Component
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://inventory-service").build();
}
@Bulkhead(name = "inventory", fallbackMethod = "stockUnavailable")
public StockStatus getStock(String productId) {
return restClient.get()
.uri("/stock/{id}", productId)
.retrieve()
.body(StockStatus.class);
}
// Called when the bulkhead is full (or the call fails)
private StockStatus stockUnavailable(String productId, BulkheadFullException ex) {
return StockStatus.unknown(productId);
}
}
When 10 inventory calls are already running, the 11th waits up to 100 ms for a free slot. If none frees up, Resilience4j throws a BulkheadFullException, and the fallback returns “stock unknown” at once. The order page stays fast instead of joining the queue of stuck requests.
BulkheadFullException runs only when the bulkhead is full; use Exception or Throwable to also handle other failures.8. Thread Pool Bulkhead with Resilience4j
The thread pool bulkhead runs calls on a dedicated pool of threads. The caller’s thread is not blocked, so the protected method must return a CompletableFuture (or another CompletionStage):
resilience4j:
thread-pool-bulkhead:
instances:
recommendations:
core-thread-pool-size: 2
max-thread-pool-size: 4
queue-capacity: 10
keep-alive-duration: 20ms
package com.example.order.client;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
@Component
public class RecommendationClient {
private final RestClient restClient;
public RecommendationClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://recommendation-service").build();
}
@Bulkhead(name = "recommendations",
type = Bulkhead.Type.THREADPOOL,
fallbackMethod = "noRecommendations")
public CompletableFuture<List<Product>> forProduct(String productId) {
List<Product> products = restClient.get()
.uri("/recommendations?productId={id}", productId)
.retrieve()
.body(new ParameterizedTypeReference<List<Product>>() { });
return CompletableFuture.completedFuture(products);
}
private CompletableFuture<List<Product>> noRecommendations(String productId, Throwable ex) {
return CompletableFuture.completedFuture(List.of());
}
}
The body of the method runs on one of the 2–4 threads reserved for recommendations. When all threads are busy and the queue of 10 is full, new calls go straight to the fallback, and the page simply shows no suggestions.
9. Semaphore vs Thread Pool Bulkhead
| Semaphore bulkhead | Thread pool bulkhead | |
|---|---|---|
| How it limits | Counts concurrent calls; calls run on the caller’s thread | Runs calls on a separate, fixed pool of threads |
| Method return type | Any | CompletableFuture / CompletionStage |
| Overhead | Very low | Higher: thread switching and extra threads |
| Caller thread | Blocked while the call runs | Free; the result arrives later |
| Waiting room | Short optional wait (max-wait-duration) | A bounded queue (queue-capacity) |
| Good for | Most cases, including reactive and virtual-thread code | Slow blocking calls you want to run in the background |
Rule of thumb: start with the semaphore bulkhead. It is simpler and cheaper, and it is the better fit for reactive code and Java virtual threads, where creating threads is cheap but the downstream service’s capacity is still limited.
10. How to Size a Bulkhead
A bulkhead that is too small rejects good traffic; one that is too large does not protect anything. A simple starting point is Little’s law:
concurrent calls needed = requests per second x average response time (seconds)
Example:
50 requests/second x 0.2 seconds = 10 concurrent calls
Add headroom for spikes (about 50%) -> max-concurrent-calls = 15
- Measure real traffic and response times, ideally at peak hours, using your metrics.
- Respect the downstream limit: never allow more concurrent calls than the other service can handle.
- Keep the total sensible: the sum of all bulkheads should fit within your service’s thread and connection limits.
- Prioritize: give critical paths, such as checkout, more room than optional features, such as recommendations.
- Load test and adjust: confirm in a test environment that a slow dependency fills only its own bulkhead.
11. Combining Bulkheads with Other Resilience Patterns
Bulkheads limit how much a dependency can take, but they do not decide how long to wait or when to stop calling. Combine them with other patterns:
| Pattern | Question it answers | Together with bulkheads |
|---|---|---|
| Timeout | How long do we wait? | Slots are freed quickly instead of being held by stuck calls |
| Circuit breaker | Should we stop calling a failing service? | Stops sending work into a bulkhead that would only fill up again |
| Retry | Should we try again? | Use carefully: retries add load to a bulkhead that is already full |
| Rate limiter | How many calls per second are allowed? | Limits the request rate, while the bulkhead limits concurrent calls |
| Fallback | What do we return instead? | Gives users a degraded but useful answer when the bulkhead rejects a call |
@Retry(name = "inventory")
@CircuitBreaker(name = "inventory", fallbackMethod = "stockUnavailable")
@Bulkhead(name = "inventory")
public StockStatus getStock(String productId) {
return restClient.get()
.uri("/stock/{id}", productId)
.retrieve()
.body(StockStatus.class);
}
By default, Resilience4j applies the annotations in this order, from outside to inside: Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead. The bulkhead is closest to the actual call, so each retry attempt also needs a free slot. Always configure timeouts on the HTTP client as well, so a slot is never held for too long.
12. Bulkheads Beyond Code: Connection Pools and Deployments
12.1 Separate connection pools
Database connections are a classic shared resource. A heavy reporting query can take every connection and block customer checkouts. Give each workload its own pool:
app:
datasource:
orders:
jdbc-url: jdbc:postgresql://db:5432/shop
maximum-pool-size: 20 # customer-facing work
reporting:
jdbc-url: jdbc:postgresql://db-replica:5432/shop
maximum-pool-size: 5 # heavy reports cannot take more than 5
Each pool is created as its own DataSource bean, bound with @ConfigurationProperties. The same idea applies to HTTP clients: give each downstream service its own connection pool with its own limits.
12.2 Separate deployments
At the infrastructure level, run critical and non-critical workloads on separate instances, node pools or namespaces, each with fixed resource limits:
spec:
replicas: 3
template:
spec:
containers:
- name: checkout-service
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
Resource limits stop one container from starving its neighbors on the same machine, and separate deployments mean a crash or memory leak in a batch job cannot take checkout down with it.
13. Monitoring and Testing Bulkheads
With Spring Boot Actuator and Micrometer, Resilience4j publishes bulkhead metrics automatically. Useful ones include:
| Metric | What it tells you |
|---|---|
resilience4j.bulkhead.available.concurrent.calls | Free slots in a semaphore bulkhead; close to 0 means it is nearly full |
resilience4j.bulkhead.max.allowed.concurrent.calls | The configured limit |
resilience4j.bulkhead.thread.pool.size | Current threads in a thread pool bulkhead |
resilience4j.bulkhead.queue.depth | Calls waiting in a thread pool bulkhead’s queue |
Set alerts when a bulkhead is often full or rejects many calls: it means either the dependency is in trouble or the bulkhead is too small.
To test a bulkhead, simulate a slow dependency, for example with WireMock adding a fixed delay, send many requests in parallel, and check that:
- Calls beyond the limit are rejected quickly and reach the fallback.
- Calls to other dependencies still succeed at normal speed.
- The bulkhead recovers as soon as the slow dependency speeds up again.
14. Key Benefits of the Bulkhead Pattern
- Failure containment: a problem in one dependency cannot bring down the entire system.
- Graceful degradation: parts of the system keep working while others fail, for example a product page without recommendations.
- Better resource allocation: critical features can be guaranteed capacity based on their importance.
- Higher availability: preventing cascading failures improves overall uptime.
- Fair sharing: one noisy client or tenant cannot use up capacity meant for everyone.
- Faster failures: rejected calls fail in milliseconds instead of hanging, so users get a quick answer.
15. Drawbacks and Trade-offs
| Trade-off | Why it matters | How to manage it |
|---|---|---|
| Unused capacity | Reserved threads or connections may sit idle while another bulkhead is full | Size bulkheads from real traffic and review them regularly |
| More configuration | Every bulkhead needs limits that must be tuned | Start with a few bulkheads for the riskiest dependencies |
| Rejected requests | Some valid requests are refused during spikes | Provide useful fallbacks and monitor rejection rates |
| Thread overhead | Thread pool bulkheads add threads and context switching | Prefer semaphore bulkheads where possible |
16. Best Practices
- Identify critical boundaries: isolate dependencies based on how critical they are and who uses them.
- Set sensible limits: size each bulkhead (concurrent calls, threads, queue size) from performance tests and capacity planning.
- Always provide a fallback, such as cached data, a default value or a friendly message.
- Keep waiting short: a small
max-wait-durationor queue keeps rejections fast. - Combine with other patterns: bulkheads are most powerful together with timeouts, circuit breakers, retries and fallbacks.
- Protect the most important paths first, such as checkout and payment.
- Monitor and alert on bulkhead usage and rejections.
- Isolate at several levels: threads and connections in code, plus separate deployments for critical services.
You do not need to build bulkheads from scratch. Resilience4j (Java) provides semaphore and thread pool bulkheads. Hystrix, which popularized thread pool isolation, is now in maintenance mode but remains conceptually important. Polly offers a bulkhead policy for .NET, and service meshes such as Istio can limit connections and pending requests per service.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| One shared pool for all dependencies | One slow service blocks calls to all the others | Give each important dependency its own bulkhead |
| Unbounded queues | Requests pile up in memory and wait far too long | Use small, bounded queues and reject the rest |
| No timeouts | Slots stay occupied by calls that will never finish | Set connect and read timeouts on every client |
| No fallback | Users see errors instead of a degraded page | Return cached, default or partial data |
| Limits picked at random | Too small rejects good traffic; too large protects nothing | Size from measurements and load tests |
| Retrying into a full bulkhead | Retries add even more load to an overloaded dependency | Retry rarely, with backoff, behind a circuit breaker |
| Bulkheads without monitoring | Problems are noticed only when users complain | Alert on usage and rejection rates |
18. Summary
The Bulkhead pattern is a fundamental technique for building robust, fault-tolerant microservices. Like the watertight compartments of a ship, it separates resources, such as threads, connections and service instances, so that a slow or failing dependency can only exhaust its own share. With Resilience4j you can add semaphore or thread pool bulkheads to Spring Boot with a few lines of configuration, provide fallbacks for a graceful experience, and monitor them through Actuator. Size bulkheads from real measurements, combine them with timeouts and circuit breakers, and isolate critical workloads at the infrastructure level too. In the unpredictable world of distributed systems, these compartments are not just a best practice; they are essential for keeping your application stable and available. Continue with the Circuit Breaker Pattern to complete your resilience toolkit.
19. Frequently Asked Questions (FAQ)
Q1. What problem does the Bulkhead pattern solve?
It stops one slow or failing dependency from using up shared resources, such as threads or connections, and taking unrelated features down with it.
Without bulkheads, a single slow service can occupy every thread in your application, so even healthy features stop working. With bulkheads, each dependency has its own limited share, and a problem stays inside that share.
Q2. Why is it called the Bulkhead pattern?
The name comes from ships. A bulkhead is a watertight wall that divides a ship’s hull into compartments. If the hull is damaged, only one compartment floods and the ship stays afloat.
In software, the compartments are separate pools of resources, and the “water” is overload or failure that should stay where it started.
Q3. What is the difference between the Bulkhead pattern and a circuit breaker?
| Bulkhead | Circuit breaker | |
|---|---|---|
| Limits | How many calls can run at the same time | Whether calls are made at all |
| Reacts to | Too many concurrent calls | A high failure or slow-call rate |
| Main goal | Contain resource usage | Stop calling a service that is failing |
They complement each other: the bulkhead limits damage while a service struggles, and the circuit breaker stops the calls once it is clearly failing.
Q4. What is the difference between a bulkhead and a rate limiter?
- A rate limiter controls how many calls per time period are allowed, for example 100 per second.
- A bulkhead controls how many calls run at the same moment, for example 10 at once.
If calls become slow, a rate limiter still lets 100 per second through, and they can pile up. A bulkhead caps the pile-up itself. Use rate limiting to protect against too much traffic and bulkheads to protect against slow dependencies.
Q5. Should I use a semaphore bulkhead or a thread pool bulkhead?
Start with the semaphore bulkhead in most cases. It is simple, has very little overhead and works with any return type, including reactive types and code running on virtual threads.
Choose the thread pool bulkhead when you want slow, blocking calls to run on separate threads, so the caller’s thread is free to do other work. It requires methods to return a CompletableFuture.
Q6. What happens when a bulkhead is full?
New calls either wait a short configured time for a free slot, or are rejected immediately. In Resilience4j, a rejected call throws a BulkheadFullException.
You should catch this with a fallback that returns something useful, such as cached data, a default value or a friendly “please try again” message, instead of showing an error.
Q7. How many bulkheads should a service have?
Usually one per important remote dependency, such as one for inventory calls and one for payment calls. Dependencies that are optional, such as recommendations, especially benefit from their own small bulkhead.
Avoid creating dozens of tiny bulkheads; each one reserves capacity and needs tuning. Start with the riskiest or most critical dependencies and add more only when monitoring shows a need.
Q8. Does the Bulkhead pattern still matter with Java virtual threads?
Yes. Virtual threads make threads cheap, so a slow service no longer exhausts your thread pool so easily. But the downstream service, database connections and memory are still limited.
Without a limit, virtual threads can send thousands of concurrent calls to a struggling service and make things worse. A semaphore bulkhead is a natural fit for virtual-thread code, because it limits concurrency without needing a thread pool.
Q9. Can I use bulkheads with reactive Spring WebFlux code?
Yes. Resilience4j provides Reactor operators, and the @Bulkhead annotation also works on methods returning Mono or Flux:
@Bulkhead(name = "inventory", fallbackMethod = "stockUnavailable")
public Mono<StockStatus> getStock(String productId) {
return webClient.get()
.uri("/stock/{id}", productId)
.retrieve()
.bodyToMono(StockStatus.class);
}
Use the semaphore type with reactive code; the thread pool type is meant for blocking calls.
Q10. Is Hystrix still a good choice for bulkheads?
No. Netflix put Hystrix into maintenance mode in 2018, and it is no longer part of current Spring Cloud releases. Its ideas, such as thread pool isolation and fallbacks, live on in Resilience4j, which is lightweight, actively maintained and integrates well with Spring Boot.
If you maintain an old Hystrix-based application, plan a migration to Resilience4j.
Q11. How do bulkheads help with noisy neighbors?
A noisy neighbor is a client, tenant or feature that uses far more resources than others and slows everyone down. Bulkheads give each group its own capacity:
- Separate bulkheads for free and paying customers.
- Separate pools for interactive users and batch jobs.
- Separate instances for a large partner whose traffic is very spiky.
A spike from one group then fills only its own share, and everyone else keeps normal performance.
Q12. Where should bulkheads be placed: in the caller or in the called service?
Mostly in the caller. The caller is the one whose threads and connections get stuck waiting, so it protects itself by limiting calls to each dependency.
The called service can also protect itself, for example by separating pools for different endpoints or clients, and by rate limiting incoming requests. Using both gives the strongest protection.
Q13. Do bulkheads waste resources?
They can, a little. Reserved capacity in one bulkhead cannot be borrowed by another, so some threads or connections may sit idle while another bulkhead is full.
This is a deliberate trade-off: you give up a small amount of efficiency for much better stability. Keep the waste low by sizing bulkheads from real traffic, reviewing them regularly, and preferring semaphore bulkheads, which reserve no threads at all.
Q14. How do I choose good values for max-concurrent-calls?
Use measurements, not guesses. A simple starting formula is:
max concurrent calls = peak requests per second x typical response time (seconds) x 1.5
For 40 requests per second with a 0.25-second response time, that is 40 × 0.25 × 1.5 = 15. Then check that the downstream service can handle that many parallel calls, run a load test, and adjust using your monitoring data.
Q15. Is a database connection pool a bulkhead?
Yes, in effect. A connection pool such as HikariCP limits how many connections can be used at once, so it already caps database load from one application.
It becomes a true bulkhead between features when you use separate pools for different workloads, for example a small pool for heavy reports and a larger one for customer orders, so slow reports cannot take every connection.
Q16. How do Kubernetes resource limits relate to bulkheads?
They are infrastructure-level bulkheads. CPU and memory limits stop one container from starving others on the same node, and separate deployments, node pools or namespaces keep failures of one workload away from another.
They complement code-level bulkheads: Kubernetes isolates whole services, while Resilience4j isolates individual dependencies inside a service.
Q17. Should the fallback call another remote service?
Preferably not. A fallback runs exactly when things are going wrong, so it should be fast and reliable. Good fallbacks:
- Return cached or slightly old data.
- Return a safe default, such as an empty list of recommendations.
- Return a clear message, such as “Stock information is temporarily unavailable”.
If a fallback must call another service, protect that call with its own bulkhead and timeout too.
Q18. How can I test that my bulkheads work?
- Simulate a slow dependency, for example with WireMock and a fixed response delay.
- Send more parallel requests than the bulkhead allows.
- Check that extra calls are rejected quickly and receive the fallback.
- Check that calls to other dependencies still succeed at normal speed.
- Remove the delay and confirm the bulkhead recovers.
Repeat the test in a load-testing environment before major sales or launches.
Q19. Can the Bulkhead pattern be used outside microservices?
Yes. The idea of isolating resources applies to any system:
- A monolith can use separate thread pools for different external APIs.
- A web server can reserve capacity for health checks and admin pages.
- A message consumer can use separate consumer groups or queues for urgent and bulk messages.
Microservices simply make it more important, because they depend on many network calls.
Q20. What are the signs that my system needs bulkheads?
- One slow external service makes unrelated pages slow or unavailable.
- Thread dumps during incidents show most threads waiting on the same dependency.
- Connection pools are often exhausted by one type of work, such as reports.
- Optional features, like recommendations, can break critical ones, like checkout.
- Incidents spread from one service to the services that call it.
If any of these sound familiar, start by adding bulkheads to the dependency involved.