Thursday, 21 August 2025

Multi-Module Spring Boot Microservices Project with JWT

Multi-Module Spring Boot Microservices Project with JWT (POC, no DB) in IntelliJ step by step.

you’ll have one root Maven project and inside it multiple submodules (auth-service, api-gateway, order-service, product-service)


---


๐Ÿ›  Step 1: Create Parent Project


1. In IntelliJ → New Project → Choose Maven.



2. Name it jwt-microservices-poc.



3. Select Packaging: pom.



4. This parent project will not have code, only manage dependencies.




pom.xml (Parent)


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

         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>


    <groupId>com.neeraj</groupId>

    <artifactId>jwt-microservices-poc</artifactId>

    <version>1.0-SNAPSHOT</version>

    <packaging>pom</packaging>


    <modules>

        <module>auth-service</module>

        <module>api-gateway</module>

        <module>order-service</module>

        <module>product-service</module>

    </modules>


    <properties>

        <java.version>11</java.version>

        <spring.boot.version>2.7.18</spring.boot.version>

        <spring.cloud.version>2021.0.8</spring.cloud.version>

    </properties>


    <dependencyManagement>

        <dependencies>

            <dependency>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-dependencies</artifactId>

                <version>${spring.boot.version}</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

            <dependency>

                <groupId>org.springframework.cloud</groupId>

                <artifactId>spring-cloud-dependencies</artifactId>

                <version>${spring.cloud.version}</version>

                <type>pom</type>

                <scope>import</scope>

            </dependency>

        </dependencies>

    </dependencyManagement>

</project>



---


๐Ÿ›  Step 2: Create Submodules


Inside jwt-microservices-poc, create 4 Maven Modules:


auth-service


api-gateway


order-service


product-service



Each submodule will have its own pom.xml.



---


๐Ÿ›  Step 3: Auth Service (JWT Issuer)


๐Ÿ“ auth-service/pom.xml


<project>

    <parent>

        <groupId>com.neeraj</groupId>

        <artifactId>jwt-microservices-poc</artifactId>

        <version>1.0-SNAPSHOT</version>

    </parent>

    <artifactId>auth-service</artifactId>


    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-security</artifactId>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-api</artifactId>

            <version>0.11.5</version>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-impl</artifactId>

            <version>0.11.5</version>

            <scope>runtime</scope>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-jackson</artifactId>

            <version>0.11.5</version>

            <scope>runtime</scope>

        </dependency>

    </dependencies>

</project>


๐Ÿ‘‰ Same SecurityConfig, JwtUtil, and AuthController as I gave you earlier.

Set server.port=8081 in application.properties.



---


๐Ÿ›  Step 4: API Gateway


๐Ÿ“ api-gateway/pom.xml


<project>

    <parent>

        <groupId>com.neeraj</groupId>

        <artifactId>jwt-microservices-poc</artifactId>

        <version>1.0-SNAPSHOT</version>

    </parent>

    <artifactId>api-gateway</artifactId>


    <dependencies>

        <dependency>

            <groupId>org.springframework.cloud</groupId>

            <artifactId>spring-cloud-starter-gateway</artifactId>

        </dependency>

        <dependency>

            <groupId>io.jsonwebtoken</groupId>

            <artifactId>jjwt-api</artifactId>

            <version>0.11.5</version>

        </dependency>

    </dependencies>

</project>


๐Ÿ“ application.yml


server:

  port: 8080


spring:

  cloud:

    gateway:

      routes:

        - id: order-service

          uri: http://localhost:8082/

          predicates:

            - Path=/order/**

        - id: product-service

          uri: http://localhost:8083/

          predicates:

            - Path=/product/**


๐Ÿ“ Add JwtAuthFilter as shown before.



---


๐Ÿ›  Step 5: Order Service


๐Ÿ“ order-service/pom.xml


<project>

    <parent>

        <groupId>com.neeraj</groupId>

        <artifactId>jwt-microservices-poc</artifactId>

        <version>1.0-SNAPSHOT</version>

    </parent>

    <artifactId>order-service</artifactId>


    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

    </dependencies>

</project>


๐Ÿ“ OrderController.java


@RestController

@RequestMapping("/order")

public class OrderController {

    @GetMapping("/details")

    public String getOrder() {

        return "Order details fetched successfully!";

    }

}


๐Ÿ“ application.properties


server.port=8082



---


๐Ÿ›  Step 6: Product Service


๐Ÿ“ product-service/pom.xml


<project>

    <parent>

        <groupId>com.neeraj</groupId>

        <artifactId>jwt-microservices-poc</artifactId>

        <version>1.0-SNAPSHOT</version>

    </parent>

    <artifactId>product-service</artifactId>


    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

    </dependencies>

</project>


๐Ÿ“ ProductController.java


@RestController

@RequestMapping("/product")

public class ProductController {

    @GetMapping("/list")

    public String getProducts() {

        return "Product list fetched successfully!";

    }

}


๐Ÿ“ application.properties


server.port=8083



---


๐Ÿ›  Step 7: Run & Test (Stepwise in IntelliJ)


1. Right-click Parent Project → Run ‘mvn clean install’ (this builds all modules).



2. Run AuthServiceApplication → port 8081.



3. Run ApiGatewayApplication → port 8080.



4. Run OrderServiceApplication → port 8082.



5. Run ProductServiceApplication → port 8083.





---


๐Ÿ›  Step 8: Verify


๐Ÿ‘‰ Get Token:


curl -X POST http://localhost:8081/auth/login \

 -H "Content-Type: application/json" \

 -d '{"username":"neeraj", "password":"password"}'


๐Ÿ‘‰ Call Order Service via Gateway:


curl -X GET http://localhost:8080/order/details \

 -H "Authorization: Bearer <TOKEN>"


๐Ÿ‘‰ Call Product Service via Gateway:


curl -X GET http://localhost:8080/product/list \

 -H "Authorization: Bearer <TOKEN>"


✅ Both return data when token is valid.





Sunday, 17 August 2025

๐Ÿ”’ Bulkhead Pattern in Microservices with Spring Boot (Resilience4j)

 ๐Ÿ”’ Bulkhead Pattern in Microservices with Spring Boot (Resilience4j)


What is the Bulkhead Pattern?

The Bulkhead pattern isolates different parts of your application so that a failure in one part does not bring down the whole system — just like watertight compartments in a ship.

In microservices, this usually means isolating:

  • Different external calls (like DB, REST APIs),

  • Or threads for different tasks.


How to Implement Bulkhead Pattern in Spring Boot

We can implement Bulkhead using Resilience4j – a fault-tolerance library designed for Java 8 and functional programming.


๐Ÿ› ️ Step-by-Step Implementation

1. Add Resilience4j Dependencies


In your Spring Boot pom.xml:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
</dependency>


Also add actuator (optional but useful):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2. Basic Bulkhead Usage Example (Thread Pool Bulkhead)

❗Scenario: A method making an external REST API call is bulkheaded.

import io.github.resilience4j.bulkhead.annotation.Bulkhead; import org.springframework.stereotype.Service; @Service public class ExternalCallService { @Bulkhead(name = "externalApiBulkhead", type = Bulkhead.Type.THREADPOOL) public String callExternalService() { // Simulated external service call return "Success from external service"; } }

3. Configure Bulkhead Properties in application.yml

resilience4j: bulkhead: instances: externalApiBulkhead: max-concurrent-calls: 5 max-wait-duration: 100ms thread-pool-bulkhead: instances: externalApiBulkhead: core-thread-pool-size: 4 max-thread-pool-size: 8 queue-capacity: 20 keep-alive-duration: 30s

4. Add Fallback Method (Optional)

You can add fallback with @Bulkhead using Spring's @Recover or use @Retry or @CircuitBreaker with it too.

@Bulkhead(name = "externalApiBulkhead", fallbackMethod = "fallbackMethod") public String callExternalService() { // ... } public String fallbackMethod(Throwable t) { return "Service temporarily unavailable"; }

5. Monitoring with Actuator

If you added Spring Boot Actuator:

management: endpoints: web: exposure: include: resilience4j.bulkhead

You can now hit this endpoint to monitor:

GET /actuator/resilience4j/bulkhead

✅ When to Use Which Type?

TypeUse When...
Semaphore (default)You want simple thread isolation.
ThreadPoolYou want to run the method asynchronously in separate thread pool. Better control for latency issues.

๐Ÿ“Œ Real Use Case: Calling Another Microservice

@Bulkhead(name = "userServiceBulkhead", type = Bulkhead.Type.THREADPOOL) public UserDto getUserById(String userId) { return webClient.get() .uri("http://user-service/users/{id}", userId) .retrieve() .bodyToMono(UserDto.class) .block(); }

If you're building a microservice architecture and using WebClient, Feign, or RestTemplate, you can wrap your service calls with Resilience4j Bulkhead to prevent cascading failures and resource exhaustion.


๐Ÿ”š Summary

  • Use Resilience4j for implementing Bulkhead in Spring Boot.

  • Protect your microservices from overloading or crashing due to dependencies.

  • Combine with other patterns like Circuit Breaker and Retry for robust fault-tolerance.


How is the Bulkhead pattern different from Resilience (or Resilience4j in general)?

Let’s clear that up ๐Ÿ‘‡


๐Ÿ” Difference Between Bulkhead and Resilience (Resilience4j)

AspectBulkhead PatternResilience4j (Framework)
What it isA design pattern to isolate failuresA Java library to implement various resilience patterns
GoalPrevent failure in one part from affecting othersProvide multiple tools (patterns) to build resilient apps
ScopeFocused on concurrency/resource isolationCovers a broad range: retries, circuit breakers, rate limiters, etc.
Implements what?Thread pool or semaphore isolationImplements Bulkhead, Circuit Breaker, Retry, Rate Limiter, etc.
Example UseLimit concurrent access to slow DB/APIHandle failures, retries, fallbacks, timeouts, bulkheads, etc.
How it's usedAs one component inside Resilience4jAs a complete library with annotations/configs for all patterns

๐Ÿ”ง In Code Terms

Bulkhead is just one module of Resilience4j:

@Bulkhead(name = "myBulkhead") public String callSomething() { // Protected logic }

But Resilience4j also allows this:

@Retry(name = "myRetry") @CircuitBreaker(name = "myCB") @RateLimiter(name = "myRateLimiter") @Bulkhead(name = "myBulkhead") public String callService() { return restClient.call(); }

๐Ÿง  Analogy

Resilience4j = Swiss Army Knife ๐Ÿ› ️
Bulkhead = One specific tool (like a screwdriver) ๐Ÿ”ฉ

You use Resilience4j to implement the Bulkhead pattern, along with other resilience techniques like retry, fallback, circuit breaker, etc.


๐Ÿงต Thread Pool Bulkhead vs. Circuit Breaker

PatternMain Purpose
BulkheadPrevent too many concurrent executions
Circuit BreakerStop calling failing service until it recovers
RetryTry again after failure
Rate LimiterLimit number of calls per time unit

✅ Summary

  • Bulkhead is a pattern to isolate failures using thread pools or semaphores.

  • Resilience4j is a library that helps you implement Bulkhead + other patterns.

  • Bulkhead lives inside Resilience4j.


 which resilience strategy is "best" and looking for tricky insights. Let’s dive into it.


✅ Which Fault Tolerance Pattern Is Best in Microservices?

๐Ÿ† Answer: There's No One-Size-Fits-All

The best pattern depends on the type of failure you're defending against.

Let’s break this down by pattern vs failure type:

Failure ScenarioBest Pattern(s)
Remote service is down or unstableCircuit Breaker
Remote service is slow / unresponsive (latency issue)Timeout + Bulkhead + Circuit Breaker
Service is overloaded with too many requestsRate Limiter + Bulkhead
Occasional transient errors (e.g. network hiccups)Retry + Timeout
Cascading failure risk due to shared resourcesBulkhead
Service not failing, but you want to protect itRate Limiter

๐Ÿง  Some Tricky and Smart Points (That Interviewers Love)

1. Use Bulkhead + CircuitBreaker Together for Maximum Safety

  • Bulkhead protects your threads (isolates services).

  • CircuitBreaker protects from bad services (fail fast).

Combine them for both resource isolation and failure short-circuiting.


2. Retries Can Be Dangerous if Not Combined with Timeout

  • Retry without timeout = blocking threads longer.

  • Retry + Timeout + Backoff = safer.

retry: max-attempts: 3 wait-duration: 1s exponential-backoff-multiplier: 2

Tricky trap: Retry on a slow service = more traffic = worse overload.


3. ThreadPool Bulkhead is Better Than Semaphore in Latency-Prone Systems

  • Semaphore is synchronous → if a slow call hangs, your main threads hang.

  • Thread pool bulkhead executes slow calls in separate threads.

Use Bulkhead.Type.THREADPOOL when calling external APIs or slow databases.


4. Don’t Retry on All Exceptions!

  • Retry on IOException or TimeoutException

  • ❌ Don't retry on Http 400 (bad request), IllegalArgumentException etc.

Smart config: Use retry-exceptions and ignore-exceptions filters.


5. Fallback is Your Last Line of Defense

  • Always define a fallback method where possible.

  • Fallbacks can return:

    • Cached data

    • Empty list / default value

    • Message: "Temporarily unavailable"

Without fallback, your resilience chain is incomplete.


6. Resilience ≠ Performance

Too many layers (retry + timeout + bulkhead + circuit breaker + rate limiter) can:

  • Slow down services.

  • Waste resources (extra threads, memory).

Use just what's needed for that specific service call.


๐Ÿ”š Best Practice Combo Matrix

Use CaseResilience Pattern Combo
Calling external payment APIRetry + Timeout + CircuitBreaker + Bulkhead
Slow reporting jobTimeout + ThreadPool Bulkhead
User login with auth serviceCircuitBreaker + Fallback + Rate Limiter
Internal DB latencyTimeout + Retry (if retry-safe)
Event publishing (e.g. Kafka)Retry + CircuitBreaker + Fallback

๐Ÿ”ฅ Pro Tips for Interviews / Design Discussions

  1. "I don’t apply all patterns everywhere — I profile the risk first."

  2. "ThreadPool bulkhead gives isolation AND protects main thread pool."

  3. "Retries amplify problems if used on slow/unhealthy systems."

  4. "Fallbacks should be fast and never throw — they’re the seatbelt, not the airbag."

  5. "Timeout is your most important defense — default is often too high or ignored."


Spring Boot 3 microservice using Resilience4j to demonstrate:

  • ✅ Retry

  • ✅ Circuit Breaker

  • ✅ Bulkhead

  • ✅ Fallback

  • ✅ Actuator for metrics

We'll simulate an external call that sometimes fails to see how resilience patterns help.


๐Ÿ“ Project Structure

resilience-demo/ ├── src/ │ └── main/ │ └── java/com/example/demo/ │ ├── DemoApplication.java │ ├── controller/ApiController.java │ └── service/RemoteService.java ├── pom.xml └── application.yml

๐Ÿ”น 1. pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" ...> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>resilience-demo</artifactId> <version>1.0</version> <name>Resilience Demo</name> <properties> <java.version>17</java.version> <spring.boot.version>3.1.5</spring.boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

๐Ÿ”น 2. application.yml

server: port: 8080 management: endpoints: web: exposure: include: "*" resilience4j: retry: instances: backendService: max-attempts: 3 wait-duration: 1s circuitbreaker: instances: backendService: sliding-window-size: 5 failure-rate-threshold: 50 wait-duration-in-open-state: 5s bulkhead: instances: backendService: max-concurrent-calls: 5 max-wait-duration: 500ms

๐Ÿ”น 3. DemoApplication.java

package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

๐Ÿ”น 4. ApiController.java

package com.example.demo.controller; import com.example.demo.service.RemoteService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ApiController { private final RemoteService remoteService; public ApiController(RemoteService remoteService) { this.remoteService = remoteService; } @GetMapping("/api/data") public String fetchData() { return remoteService.callRemoteService(); } }

๐Ÿ”น 5. RemoteService.java

package com.example.demo.service; import io.github.resilience4j.bulkhead.annotation.Bulkhead; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import org.springframework.stereotype.Service; import java.util.Random; @Service public class RemoteService { private final Random random = new Random(); @Retry(name = "backendService", fallbackMethod = "fallback") @CircuitBreaker(name = "backendService", fallbackMethod = "fallback") @Bulkhead(name = "backendService", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallback") public String callRemoteService() { if (random.nextInt(10) < 7) { throw new RuntimeException("Simulated failure"); } return "Success from remote service"; } public String fallback(Throwable t) { return "Fallback response: " + t.getMessage(); } }

✅ How to Test

  1. Run the project.

  2. Open browser or Postman:
    http://localhost:8080/api/data

  3. Hit it multiple times – some will succeed, some fail → fallback kicks in.

  4. Check circuit breaker state:
    http://localhost:8080/actuator/circuitbreakers


๐Ÿ“Š Metrics Endpoints (via Actuator)

  • /actuator/metrics

  • /actuator/circuitbreakers

  • /actuator/retries

  • /actuator/bulkheads


๐Ÿง   Let’s break down each annotation in your method:

@Retry(name = "backendService", fallbackMethod = "fallback") @CircuitBreaker(name = "backendService", fallbackMethod = "fallback") @Bulkhead(name = "backendService", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallback") public String callRemoteService() { ... }

You see three Resilience4j annotations, and you're right to ask: Why all three? What does each do?

Let’s go through them line by line.


๐Ÿ” 1. @Retry(name = "backendService", fallbackMethod = "fallback")

✅ What it does:

  • Automatically retries the method if it throws an exception.

  • Tries again up to 3 times (as per your application.yml config).

  • If all retries fail, it goes to the fallback method.

⚙️ Configuration from application.yml:

retry: instances: backendService: max-attempts: 3 wait-duration: 1s

๐Ÿง  Why it's useful:

  • Handles temporary network issues or intermittent failures.

  • Prevents user-facing failures for short-lived problems.

๐Ÿชค But tricky:

  • Retrying on slow services can overload them further.

  • Should not retry on 4xx errors or IllegalArgumentException — filter exceptions if needed.


⚡ 2. @CircuitBreaker(name = "backendService", fallbackMethod = "fallback")

✅ What it does:

  • Stops calling the method temporarily if too many failures happen.

  • Instead of retrying forever, it opens the circuit after 50% failure in a 5-call window (as per your config).

  • When circuit is open, calls go straight to fallback.

⚙️ Configuration from application.yml:

circuitbreaker: instances: backendService: sliding-window-size: 5 failure-rate-threshold: 50 wait-duration-in-open-state: 5s

๐Ÿง  Why it's useful:

  • Prevents cascading failure.

  • Allows a failing service to "cool off".

  • Helps maintain system stability under pressure.

๐Ÿชค Tricky:

  • Circuit opens even on business exceptions unless filtered.

  • Monitor state carefully; adjust thresholds in production.


๐Ÿ”’ 3. @Bulkhead(name = "backendService", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallback")

✅ What it does:

  • Limits concurrent access to the method — like a thread gate.

  • If more than 5 threads try to access it (based on your config), it immediately fails and goes to fallback.

  • Type SEMAPHORE means it runs in caller’s thread (use THREADPOOL for async calls).

⚙️ Configuration:

bulkhead: instances: backendService: max-concurrent-calls: 5 max-wait-duration: 500ms

๐Ÿง  Why it's useful:

  • Protects your app from being overwhelmed if the external service is slow.

  • Prevents thread exhaustion.

๐Ÿชค Tricky:

  • Too low limit can block legit traffic.

  • No queueing in SEMAPHORE mode.


๐Ÿง  Why All Three Together?

They work best as a team to cover different kinds of faults:

PatternProtects From...Behavior
RetryTemporary failures (e.g. network)Re-tries failed requests automatically
CircuitBreakerPersistent failures or slowdownsFails fast when failures increase
BulkheadThread starvation or overloadLimits concurrent executions

By using all 3:

  • You avoid retrying endlessly (CircuitBreaker)

  • Avoid overloading resources (Bulkhead)

  • Give a second chance for temporary issues (Retry)

  • And always return a graceful fallback if all else fails.


๐Ÿงช In Practice (What Happens Step-by-Step)

When a call is made to /api/data:

  1. Bulkhead checks if max-concurrent threads limit is exceeded.

    • If yes → fallback.

  2. If allowed, call proceeds.

  3. If call fails:

    • Retry tries again (up to 3 times).

    • Each failure increases circuit breaker failure count.

  4. If failure rate hits threshold:

    • Circuit opens → all future calls go straight to fallback for 5s.


 Real-time execution flowchart with logging-based timeline to understand how Retry, CircuitBreaker, and Bulkhead interact.


๐Ÿ”๐Ÿšฆ๐Ÿ”’ Real-Time Execution Flowchart

Here's a step-by-step flow for the method using all 3 patterns:

┌───────────────────────────────┐ │ User calls /api/data │ └────────────┬──────────────────┘ ▼ ┌───────────────────────────────┐ │ BULKHEAD: Check max threads │ └──────┬─────────────┬──────────┘ │ │ allowed (<= limit) exceeded │ ▼ ▼ ┌────────────────────┐ ┌────────────┐ │ Call fallback() │ │ CIRCUIT │ └────────────────────┘ │ BREAKER │ └────┬───────┘ │ Circuit is CLOSED? → Yes │ ▼ ┌────────────────────┐ │ RETRY: Try method │ └──────┬─────────────┘ │ ┌──────Fail?────────────┐ ▼ ▼ Fallback() Retry up to max-attempts ↓ Circuit failure count += 1If failure-rate > threshold ⇒ Open circuit for wait-duration

๐Ÿ“œ Real-Time Logging Timeline Example

Let’s simulate this method being called 3 times:

@Retry(name = "backendService", fallbackMethod = "fallback") @CircuitBreaker(name = "backendService", fallbackMethod = "fallback") @Bulkhead(name = "backendService", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallback") public String callRemoteService() { ... }

๐Ÿงช Simulation of 3 Consecutive Requests


๐Ÿ” 1st Request

Bulkhead: Allowed [1/5 concurrent calls] CircuitBreaker: CLOSED Attempt 1/3 → Simulated failure Retrying... Attempt 2/3 → Simulated failure Retrying... Attempt 3/3 → Simulated failure All retries failed. Calling fallback() → "Fallback response: Simulated failure" CircuitBreaker: Failure count = 1/5

๐Ÿ” 2nd Request (more users calling concurrently)

Bulkhead: Allowed [2/5 concurrent calls] CircuitBreaker: CLOSED Attempt 1/3 → Simulated success ๐ŸŽ‰ Returning response: "Success from remote service"

๐Ÿ” 3rd Request (high failures → circuit opens)

Bulkhead: Allowed [3/5 concurrent calls] CircuitBreaker: CLOSED Attempt 1/3 → Simulated failure Retrying... Attempt 2/3 → Simulated failure Retrying... Attempt 3/3 → Simulated failure Calling fallback() → "Fallback response: Simulated failure" CircuitBreaker: Failure count = 3/5 >> CircuitBreaker will open if failure rate hits 50%

๐Ÿ” 4th Request (circuit is OPEN)

CircuitBreaker: OPEN Skipping method call — redirecting to fallback() Fallback response: "Circuit is open"

๐Ÿง  Metrics You Can Monitor

Metric EndpointWhat It Shows
/actuator/circuitbreakersState: OPEN / CLOSED / HALF_OPEN
/actuator/bulkheadsHow many concurrent calls used
/actuator/retriesRetry attempts, successes, failures

✅ Summary

You now understand:

  • The execution flow from Bulkhead → CircuitBreaker → Retry.

  • What happens when failures occur.

  • How fallbacks are used at every step.

  • How to monitor live behavior via logs and actuator.

Now let's learn about Semaphore and Threadpool 

1. Semaphore – Basic Concept

Think of a semaphore like a gatekeeper with limited tickets.

It allows only a fixed number of threads to access a resource at the same time.

If more threads come, they wait until one ticket is returned.


๐Ÿ”‘ Example:
If a semaphore has 3 permits, only 3 people can enter a room. The 4th person waits until someone leaves.

๐Ÿ‘‰ In Java:

Semaphore semaphore = new Semaphore(3);

semaphore.acquire(); // take a permit
// critical section
semaphore.release(); // release permit



2. ThreadPool – Basic Concept

ThreadPool = group of reusable worker threads.
Instead of creating a new thread for every task (which is costly), we keep a pool of threads ready to execute tasks.

๐Ÿ”‘ Example:
Think of a restaurant kitchen → fixed number of cooks (threads). Customers (tasks) give orders → cook executes → next order.

๐Ÿ‘‰ In Java:

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    System.out.println("Task executed by " + Thread.currentThread().getName());
});


3. Bulkhead Pattern in Microservices

Bulkhead = like a ship’s bulkhead walls ๐Ÿšข.
If one section is flooded, the whole ship doesn’t sink.

In Microservices:

Bulkhead isolates resources (like threads, memory, connection pool) for each service/component.

If one service gets overloaded, others still run fine.



4. How Semaphore & ThreadPool help in Bulkhead

✅ With Semaphore:

Limit concurrent requests to a microservice.

Example: Allow only 10 concurrent requests to PaymentService. If the 11th comes → wait or reject.

Prevents overloading.


✅ With ThreadPool:

Assign dedicated thread pools per service/component.

Example: PaymentService gets 20 threads, OrderService gets 30 threads.

Even if PaymentService is overloaded, OrderService still has its own pool → isolation.


5. Example (Easy Microservices Scenario)

Problem:

OrderService calls PaymentService and InventoryService.

If PaymentService is slow, it may block all threads and affect InventoryService.


Solution with Bulkhead:

Use ThreadPool isolation:

Payment calls handled by a 10-thread pool.

Inventory calls handled by a 10-thread pool.


Use Semaphore isolation:

At max 5 concurrent requests to Payment API.

If 6th request comes → fail fast or wait.



๐Ÿ‘‰ Code Example (using Resilience4j Bulkhead):

// ThreadPool Bulkhead
BulkheadConfig config = BulkheadConfig.custom()
        .maxConcurrentCalls(5) // semaphore-like behavior
        .maxWaitDuration(Duration.ofMillis(500))
        .build();

Bulkhead bulkhead = Bulkhead.of("paymentService", config);

Supplier<String> decoratedSupplier =
        Bulkhead.decorateSupplier(bulkhead, () -> paymentClient.makePayment());

// Call
Try<String> result = Try.ofSupplier(decoratedSupplier);



 Tricky Points (Asked in Interviews ๐Ÿš€)


1. Semaphore vs ThreadPool in Bulkhead?

Semaphore limits number of concurrent calls.

ThreadPool gives dedicated resources (threads) to avoid starvation.



2. Why Bulkhead needed if Circuit Breaker is there?

Circuit Breaker protects against failing calls.

Bulkhead protects against resource exhaustion. Both are complementary.



3. What happens if no Bulkhead?

One slow/downstream service can consume all threads → service unresponsive.



4. Which pattern with Async calls?

ThreadPool Bulkhead is better, since async calls need dedicated workers.


✅ In short:

Semaphore → limit concurrent requests.

ThreadPool → dedicate resources.

Bulkhead → prevents one bad service from sinking the whole system.