Advanced Java & Spring Boot · Lesson 13 of 16 · Updated ·
Calling Other Services — OpenFeign, Timeouts and Resilience4j Circuit Breakers
order-service must ask course-api for a course's title and fee before accepting an order. Calls over the network are slow and can fail — so we need a clean way to call, and a plan for when the other service is down.
1. OpenFeign: an HTTP client you don't write
// File: order-service/src/main/java/com/skills101/orders/CourseDto.java
package com.skills101.orders;
// The fields we need from course-api's JSON (extra fields are ignored)
public record CourseDto(Long id, String title, int fee) {
}
// File: order-service/src/main/java/com/skills101/orders/CourseClient.java
package com.skills101.orders;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// Just an interface: Feign writes the HTTP call. "course-api" is looked up in Eureka.
@FeignClient(name = "course-api", fallback = CourseClientFallback.class)
public interface CourseClient {
@GetMapping("/api/courses/{id}")
CourseDto getCourse(@PathVariable("id") Long id);
}
// File: order-service/src/main/java/com/skills101/orders/OrderServiceApplication.java
package com.skills101.orders;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // create implementations for @FeignClient interfaces
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
- You write an interface with Spring MVC annotations; Feign generates the code that sends
GET /api/courses/{id}and turns the JSON into aCourseDto. name = "course-api"is looked up in Eureka, so there's no host name in the code.CourseDtolists only the fields we need — unknown JSON fields (likeseats) are ignored.
(Spring's own RestClient with @HttpExchange interfaces is the modern built-in alternative; Feign is still very common in existing projects.)
2. When course-api fails: a fallback
// File: order-service/src/main/java/com/skills101/orders/CourseClientFallback.java
package com.skills101.orders;
import org.springframework.stereotype.Component;
// Used when course-api is down, slow or failing (or the circuit breaker is open)
@Component
public class CourseClientFallback implements CourseClient {
@Override
public CourseDto getCourse(Long id) {
return new CourseDto(id, null, 0); // title null = "we couldn't check right now"
}
}
// File: order-service/src/main/java/com/skills101/orders/OrderService.java
package com.skills101.orders;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public static final String TOPIC = "orders";
private final CourseClient courses;
private final KafkaTemplate<String, OrderPlacedEvent> kafka;
private final AtomicLong ids = new AtomicLong(); // a real service would use a database
public OrderService(CourseClient courses, KafkaTemplate<String, OrderPlacedEvent> kafka) {
this.courses = courses;
this.kafka = kafka;
}
public OrderResponse place(OrderRequest request) {
CourseDto course = courses.getCourse(request.courseId()); // synchronous call to course-api
long orderId = ids.incrementAndGet();
if (course.title() == null) {
// course-api is unavailable: accept the order and confirm it later instead of failing
return new OrderResponse(orderId, request.courseId(), null, 0, "ON_HOLD");
}
// asynchronous: publish an event; payments, email and analytics react in their own time
kafka.send(TOPIC, String.valueOf(orderId), new OrderPlacedEvent(orderId, course.id(), request.email(), course.fee()));
return new OrderResponse(orderId, course.id(), course.title(), course.fee(), "PLACED");
}
}
Instead of failing the customer's order with a 500 error, we accept it ON_HOLD and confirm it later. Choosing a sensible fallback is a business decision: cached data, a default value, a queued retry, or a friendly error.
3. Circuit breaker
If course-api is down, waiting for every call to time out wastes threads and makes everything slow — failures cascade. A circuit breaker watches the calls:
All of it is configuration:
# File: order-service/src/main/resources/application.properties (excerpt)
spring.cloud.openfeign.circuitbreaker.enabled=true
spring.cloud.openfeign.client.config.default.connect-timeout=2000
spring.cloud.openfeign.client.config.default.read-timeout=2000
# Resilience4j: open the breaker when half of the last 10 calls fail; try again after 10 s
resilience4j.circuitbreaker.configs.default.sliding-window-size=10
resilience4j.circuitbreaker.configs.default.failure-rate-threshold=50
resilience4j.circuitbreaker.configs.default.wait-duration-in-open-state=10s
| Pattern | Protects against |
|---|---|
| Timeout | a slow service holding your threads forever |
| Retry (with backoff) | short, temporary glitches — only for safe, idempotent calls |
| Circuit breaker | hammering a service that is clearly down |
| Bulkhead | one slow dependency using up all your threads |
| Rate limiter | too many calls per second |
Resilience4j provides all five.
4. The test
The test starts order-service with a fake course-api: course 1 answers normally, course 2 returns HTTP 500.
// File: order-service/src/test/java/com/skills101/orders/OrderServiceApplicationTests.java (excerpt)
static HttpServer stub() {
try {
HttpServer s = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
s.createContext("/api/courses/1", ex -> {
byte[] body = "{\"id\":1,\"title\":\"Java Full Stack\",\"fee\":4999,\"seats\":30}".getBytes(StandardCharsets.UTF_8);
ex.getResponseHeaders().add("Content-Type", "application/json");
ex.sendResponseHeaders(200, body.length);
ex.getResponseBody().write(body);
ex.close();
});
s.createContext("/api/courses/2", ex -> {
ex.sendResponseHeaders(500, -1);
ex.close();
});
s.start();
return s;
Ordering course 1 → 201:
<!-- expect: order-service/order-placed.json -->{ "orderId": 1, "courseId": 1, "course": "Java Full Stack", "fee": 4999, "status": "PLACED" }
Ordering course 2 (course-api fails, fallback used) → still 201, but on hold:
<!-- expect: order-service/order-on-hold.json -->{ "orderId": 2, "courseId": 2, "course": null, "fee": 0, "status": "ON_HOLD" }
Common mistakes
- No timeouts — the default can be far too long.
- Retrying non-idempotent calls (e.g. "charge card") and charging twice.
- A fallback that hides real problems without logging or metrics.
- Calling services in a long synchronous chain (A → B → C → D): latency and failure add up. Use events (next lesson) where possible.
Quick recap
- OpenFeign: declare an interface; Feign + Eureka do the HTTP call.
- Always set timeouts; add a fallback with a sensible business meaning.
- Circuit breaker: CLOSED → OPEN (fail fast) → HALF-OPEN (test) → CLOSED.
- Resilience4j: circuit breaker, retry, timeout, bulkhead, rate limiter.