6 min read
Tu aplicación tiene un endpoint de checkout que llama a tres servicios: pagos, envíos y notificaciones. Un día, el servicio de notificaciones — el menos crítico de los tres, el que manda el correo de “tu orden fue recibida” — empieza a responder lento. Nada grave, pensarías. Pero a los cinco minutos, el checkout completo deja de funcionar. Los clientes no pueden pagar.
Y la razón no tiene nada que ver con pagos: el pool de threads compartido se llenó de llamadas colgadas esperando al servicio de notificaciones, y no quedaron threads libres para procesar pagos.
Este es exactamente el problema que resuelve el patrón Bulkhead (Mamparo), el tercer patrón de resiliencia de esta serie después de Circuit Breaker y Retry.
¿Qué es el patrón Bulkhead?
El nombre viene de la construcción naval. Un barco se divide en compartimentos estancos (bulkheads) separados por paredes herméticas. Si uno se inunda, el agua no pasa a los demás compartimentos, y el barco no se hunde por completo.
Aplicado a software: en lugar de que todas tus llamadas a servicios externos compitan por el mismo pool de recursos (threads, conexiones HTTP, memoria), le asignás a cada dependencia externa su propio pool aislado. Si un servicio se satura, agota solo su pool — el resto del sistema sigue funcionando con normalidad.
El diagrama

Guía paso a paso
Paso 1 — Identificar las dependencias externas de tu servicio
Listá cada servicio, base de datos o API de terceros que tu aplicación consume. Cada una es candidata a tener su propio bulkhead.
Paso 2 — Clasificar por criticidad
No todas las dependencias merecen el mismo tamaño de pool. Pagos necesita más recursos garantizados que notificaciones, porque un fallo en pagos detiene el negocio y un fallo en notificaciones solo retrasa un correo.
Paso 3 — Asignar pools independientes
Definí un tamaño máximo de threads o conexiones concurrentes por dependencia. El total de todos los pools no debería exceder la capacidad real de tu servidor.
Paso 4 — Rechazar rápido cuando el pool está lleno
Si el pool de una dependencia está saturado, la solicitud debe fallar rápido (fail-fast) en lugar de esperar en cola indefinidamente. Combinalo con un Circuit Breaker para manejar ese rechazo con elegancia.
Paso 5 — Monitorear cada pool por separado
Necesitás métricas de uso por pool, no solo del sistema completo. Sin esta visibilidad, no vas a detectar cuál dependencia está a punto de saturar su bulkhead.
El código
El siguiente ejemplo usa pools de threads (ExecutorService) aislados por dependencia para simular Bulkhead en Java puro. Un servicio de checkout llama a tres servicios externos, cada uno con su propio pool.
import java.util.concurrent.*;
// --- Simulación de las tres dependencias externas ---
class PaymentService {
public String charge(String orderId) throws InterruptedException {
Thread.sleep(100); // rápido y estable
return "PAY-" + orderId;
}
}
class ShippingService {
public String createLabel(String orderId) throws InterruptedException {
Thread.sleep(150);
return "SHIP-" + orderId;
}
}
class NotificationService {
public void notifyCustomer(String orderId) throws InterruptedException {
// Simulamos que este servicio está teniendo problemas
Thread.sleep(5000);
}
}
// --- Bulkhead: un pool aislado por dependencia ---
class Bulkhead {
private final String name;
private final ExecutorService pool;
public Bulkhead(String name, int maxConcurrent) {
this.name = name;
this.pool = new ThreadPoolExecutor(
maxConcurrent, maxConcurrent,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(maxConcurrent) // cola acotada: sin cola infinita
);
}
public T execute(Callable operation, long timeoutMs) throws Exception {
Future future = pool.submit(operation);
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
throw new Exception("[Bulkhead:" + name + "] Pool saturado. Rechazado.");
} catch (TimeoutException e) {
future.cancel(true);
throw new Exception("[Bulkhead:" + name + "] Timeout — no se afectó a otras dependencias.");
}
}
}
// --- Servicio de checkout usando bulkheads independientes ---
class CheckoutService {
private final PaymentService paymentService = new PaymentService();
private final ShippingService shippingService = new ShippingService();
private final NotificationService notificationService = new NotificationService();
// Cada dependencia tiene su propio bulkhead con su propio límite
private final Bulkhead paymentBulkhead = new Bulkhead("Payment", 10);
private final Bulkhead shippingBulkhead = new Bulkhead("Shipping", 10);
private final Bulkhead notificationBulkhead = new Bulkhead("Notification", 3); // pool pequeño y aislado
public void checkout(String orderId) {
try {
String paymentId = paymentBulkhead.execute(() -> paymentService.charge(orderId), 2000);
System.out.println("Pago procesado: " + paymentId);
String shipmentId = shippingBulkhead.execute(() -> shippingService.createLabel(orderId), 2000);
System.out.println("Guía creada: " + shipmentId);
try {
notificationBulkhead.execute(() -> {
notificationService.notifyCustomer(orderId);
return null;
}, 1000); // timeout corto, propio de este bulkhead
System.out.println("Notificación enviada");
} catch (Exception e) {
// El fallo de notificaciones NO afecta el resultado del checkout
System.out.println("Notificación falló (no crítico): " + e.getMessage());
}
System.out.println("Checkout completado para orden: " + orderId);
} catch (Exception e) {
System.out.println("Checkout falló: " + e.getMessage());
}
}
}
// --- Main ---
public class BulkheadPatternDemo {
public static void main(String[] args) {
CheckoutService checkoutService = new CheckoutService();
System.out.println("=== Procesando checkout ORD-100 ===");
checkoutService.checkout("ORD-100");
}
}
Output esperado:
=== Procesando checkout ORD-100 ===
Pago procesado: PAY-ORD-100
Guía creada: SHIP-ORD-100
Notificación falló (no crítico): [Bulkhead:Notification] Timeout — no se afectó a otras dependencias.
Checkout completado para orden: ORD-100
El pago y el envío se completaron con éxito. El servicio de notificaciones falló por timeout, pero como tiene su propio bulkhead con su propio pool y su propio timeout, el checkout se completó igual. Sin Bulkhead, ese mismo timeout habría bloqueado un thread compartido con pagos y envíos.
Para tener en cuenta en producción
- Bulkhead complementa a Circuit Breaker, no lo reemplaza: el Bulkhead limita cuántas llamadas concurrentes puede hacer una dependencia, mientras que el Circuit Breaker decide cuándo dejar de intentar. Usados juntos, son mucho más efectivos que por separado.
- El tamaño del pool es una decisión de capacidad, no un número al azar: calculalo con datos reales de latencia y throughput (rendimiento) esperado por dependencia, no copiando el mismo valor para todas.
- Los semáforos son una alternativa más liviana: en lugar de pools de threads completos, se puede implementar Bulkhead con semáforos que limitan la concurrencia sin crear threads adicionales. Es la opción recomendada cuando tu aplicación es reactiva (WebFlux, Reactor).
- Resilience4j lo implementa listo para producción: con
ThreadPoolBulkheadySemaphoreBulkheadconfigurables declarativamente, integrado con el mismo ecosistema de Circuit Breaker y Retry que ya vimos en posts anteriores.
Conclusión
Bulkhead es, en el fondo, sentido común aplicado a la arquitectura: no pongas todos los huevos en la misma canasta de recursos. Es el patrón más simple de explicar de toda la serie de resiliencia, y sin embargo es el que con más frecuencia se ignora — hasta que un servicio de bajísima prioridad tumba todo el sistema.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
Release It! Design and Deploy Production-Ready Software — Michael T. Nygard
Resilience4j Documentation — https://resilience4j.readme.io/