Cloud Bits: Bulkhead Pattern – Isolate Failures and Sinkproof Your Services

aerial view of blue and white boat on body of water during daytime
@GetMapping("/servicea")
@ResponseStatus(HttpStatus.OK)
public String servicea() {
    return serviceaRestClient.get()
            .retrieve()
            .body(String.class);
}

@GetMapping("/serviceb")
@ResponseStatus(HttpStatus.OK)
public String serviceb() {
    return servicebRestClient.get()
            .retrieve()
            .body(String.class);
}
@Bean
public RestClient serviceaRestClient(@Value("${servicea.url}") String serviceaUrl) {
    return RestClient.builder()
            .requestFactory(new SimpleClientHttpRequestFactory())
            .baseUrl(serviceaUrl)
            .build();
}

@Bean
public RestClient servicebRestClient(@Value("${serviceb.url}") String servicebUrl) {
    return RestClient.builder()
            .requestFactory(new SimpleClientHttpRequestFactory())
            .baseUrl(servicebUrl)
            .build();
}
server:
  tomcat:
    threads:
      max: 10
    accept-count: 10
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

const (
	RequestCount = 1000
	ServiceAUrl  = "http://localhost:8000/api/v1/servicea" // The 10s delay
	ServiceBUrl  = "http://localhost:8000/api/v1/serviceb" // The healthy service
)

func main() {
	client := &http.Client{
		Timeout: 2 * time.Second,
	}

	fmt.Println("Starting Service B Heartbeat...")
	go func() {
		for {
			start := time.Now()
			_, err := client.Get(ServiceBUrl)
			if err != nil {
				fmt.Printf("❌ Service B FAILED: %v\n", err)
			} else {
				fmt.Printf("✅ Service B OK (%v)\n", time.Since(start))
			}
			time.Sleep(500 * time.Millisecond)
		}
	}()

	time.Sleep(3 * time.Second)

	fmt.Println("\n⚠️ INITIATING LOAD ON SERVICE A (The 10s delay). Watch Service B...")
	var wg sync.WaitGroup
	for range RequestCount {
		wg.Add(1)
		wg.Go(func() {
			_, _ = client.Get(ServiceAUrl)
		})
	}

	wg.Wait()
	fmt.Println("Load test complete.")
}
@Component
@Aspect
public class BulkheadAspect {

    private final Map<String, Semaphore> semaphores = new ConcurrentHashMap<>();

    @Around("@annotation(bulkhead)")
    public Object apply(ProceedingJoinPoint joinPoint, CustomBulkhead bulkhead) throws Throwable {
        Semaphore semaphore = semaphores.computeIfAbsent(
                bulkhead.name(),
                _ -> new Semaphore(bulkhead.maxConcurrent(), true)
        );
        if (semaphore.tryAcquire()) {
            try {
                return joinPoint.proceed();
            } finally {
                semaphore.release();
            }
        } else {
            throw new BulkheadFullException("Bulkhead: " + bulkhead.name() + " is full. Request rejected");
        }
    }
}
@GetMapping("/servicea")
@ResponseStatus(HttpStatus.OK)
@CustomBulkhead(name = "getServiceA")
public String servicea() {
    return serviceaRestClient.get()
            .retrieve()
            .body(String.class);
}
@GetMapping("/servicea")
@ResponseStatus(HttpStatus.OK)
@ConcurrencyLimit(limit = 5, policy = ConcurrencyLimit.ThrottlePolicy.REJECT)
public String servicea() {
    return serviceaRestClient.get()
            .retrieve()
            .body(String.class);
}
private HttpComponentsClientHttpRequestFactory createFactory(int maxConnections) {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    connectionManager.setMaxTotal(maxConnections);
    connectionManager.setDefaultMaxPerRoute(maxConnections);

    RequestConfig requestConfig = RequestConfig.custom()
            // If we can't get a connection from the pool instantly, throw an exception!
            .setConnectionRequestTimeout(Timeout.ZERO_MILLISECONDS)
            .build();

    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setDefaultRequestConfig(requestConfig)
            .build();

    return new HttpComponentsClientHttpRequestFactory(httpClient);
}

@Bean
public RestClient serviceaRestClient(
        @Value("${servicea.url}") String serviceaUrl,
        @Value("${servicea.max-connections}") int maxConn
) {
    return RestClient.builder()
            .requestFactory(createFactory(maxConn))
            .baseUrl(serviceaUrl)
            .build();
}

Leave a Reply

Your email address will not be published. Required fields are marked *