Communication — Cheat Sheet
Microservices · 7 topics. Download the PDF or the Instagram carousel and share it.
Synchronous REST Communication
REST over HTTP is the most common inter-service protocol; use OpenFeign or RestTemplate/WebClient with service discovery to call downstream services.
- ✓OpenFeign generates HTTP client implementations from annotated interfaces — cleanest API for microservices.
- ✓WebClient is non-blocking and preferred for reactive stacks or high-concurrency scenarios.
- ✓Always set connect and read timeouts on inter-service HTTP calls.
- ✓@LoadBalanced on WebClient/RestTemplate enables Eureka-based service discovery routing.
- ✓Feign + Resilience4j provides declarative circuit breaking with fallback methods.
- ✓Propagate security context (JWT, trace headers) via Feign RequestInterceptor or WebClient ExchangeFilterFunction.
// Enable Feign clients on main class or @Configuration
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication { ... }
// Feign client interface — maps to product-service
@FeignClient(
name = "product-service", // service name in Eureka
fallback = ProductClientFallback.class // circuit breaker fallback
)
public interface ProductClient {
@GetMapping("/api/products/{id}")
ProductDTO getById(@PathVariable("id") Long id);
@GetMapping("/api/products")
List<ProductDTO> listByCategory(@RequestParam("category") String category);
@PostMapping("/api/products/{id}/reserve")
ReservationDTO reserve(@PathVariable("id") Long id,
@RequestBody ReservationRequest req);
}
// Fallback class
@Component
public class ProductClientFallback implements ProductClient {
@Override
public ProductDTO getById(Long id) {
return ProductDTO.unavailable(id); // degraded response
}
// ... other fallback methods
}Synchronous vs Asynchronous Communication
Sync calls couple availability (caller waits for response); async messaging decouples services via queues or topics, improving resilience but adding eventual consistency.
- ✓Sync: caller waits for response — simple, real-time, but creates temporal coupling + cascade failures
- ✓Async: producer publishes and moves on — decoupled, resilient, but eventually consistent
- ✓Sync call chains amplify failures — circuit breakers and bulkheads limit blast radius
- ✓Async messaging absorbs traffic bursts; broker buffers messages when consumer is down
- ✓Practical rule: sync for queries (user waits), async for commands (can be eventual)
- ✓202 Accepted + polling/webhook pattern bridges async processing with user-facing APIs
// Synchronous: Order → Inventory → Warehouse call chain
// If Warehouse is down (or slow), the entire chain is affected
@Service
class OrderService {
private final InventoryClient inventoryClient; // HTTP Feign client
@CircuitBreaker(name = "inventory") // break the chain on failure
@TimeLimiter(name = "inventory") // enforce timeout
public CompletableFuture<OrderResult> place(Order order) {
// Blocks waiting for inventory response
StockResponse stock = inventoryClient.reserve(order.getSku(), order.getQty());
return CompletableFuture.completedFuture(new OrderResult(order, stock));
}
}
// Use sync when:
// - User needs an immediate answer ("is item in stock?" → must be sync)
// - Business transaction requires a real-time response
// - Request volume is low-moderate
// - Services are co-located (low latency network)Message-Driven Architecture
Services communicate by publishing and consuming messages from a broker (Kafka, RabbitMQ); producers and consumers evolve independently with no direct coupling.
- ✓Message-driven architecture decouples services temporally — producer does not wait for consumer.
- ✓Events announce state changes (broad); Commands direct a specific action to one receiver.
- ✓Publish domain events AFTER commit (TransactionalEventListener) to avoid phantom events on rollback.
- ✓The Outbox Pattern atomically persists state + event in one DB transaction, then relays to broker.
- ✓Spring Cloud Stream provides a broker-agnostic API — swap Kafka for RabbitMQ by changing config.
- ✓Idempotent consumers are required because at-least-once delivery may redeliver messages.
// Spring Cloud Stream — broker-agnostic messaging
// application.properties
spring.cloud.stream.bindings.orderPlaced-in-0.destination=orders
spring.cloud.stream.bindings.orderPlaced-in-0.group=inventory-service
spring.cloud.stream.kafka.binder.brokers=localhost:9092
// Consumer — receives OrderEvent from the "orders" topic
@Bean
public Consumer<OrderEvent> orderPlaced() {
return event -> {
log.info("Processing order {}", event.getOrderId());
inventoryService.reserve(event.getItems());
};
}
// Producer — publishes OrderEvent to the "orders" topic
@Bean
public Supplier<Flux<OrderEvent>> orderEvents() {
return () -> orderFlux; // reactive source
}
// Or imperatively — inject StreamBridge
@Service
public class OrderEventPublisher {
private final StreamBridge streamBridge;
public void publish(OrderEvent event) {
streamBridge.send("orderPlaced-out-0", event);
}
}Event-Driven Architecture
Services emit domain events when state changes; other services subscribe and react autonomously — enabling loose coupling, audit trails, and temporal decoupling.
- ✓Domain events are immutable past-tense facts (OrderPlaced, PaymentFailed); they carry enough data for consumers to act without follow-up API calls.
- ✓EDA provides temporal decoupling — the producer does not know consumers exist; consumers do not know the producer's internal state.
- ✓Consumers must be idempotent because at-least-once delivery guarantees duplicates; use an eventId deduplication table.
- ✓The Outbox Pattern ensures reliable event publishing: write the event and business data in the same DB transaction, relay separately to the broker.
- ✓EDA is eventually consistent — a window exists between event publication and consumer processing; design your system to handle this gracefully.
- ✓Prefer event-carried state transfer (events contain enough data) over event notification (consumers must call back) to avoid synchronous coupling.
// Domain event — immutable record of something that happened
@Value // Lombok immutable POJO
public class OrderPlacedEvent {
String eventId; // UUID — for idempotency
String orderId;
String customerId;
BigDecimal totalAmount;
List<OrderItem> items;
Instant occurredAt;
}
// Publish via Spring ApplicationEventPublisher (in-process)
// or Kafka/RabbitMQ (cross-service)
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final KafkaTemplate<String, OrderPlacedEvent> kafka;
@Transactional
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
OrderPlacedEvent event = OrderPlacedEvent.builder()
.eventId(UUID.randomUUID().toString())
.orderId(order.getId().toString())
.customerId(req.getCustomerId())
.totalAmount(req.getTotal())
.items(req.getItems())
.occurredAt(Instant.now())
.build();
kafka.send("order-events", order.getId().toString(), event);
return order;
}
}gRPC for Inter-Service Communication
gRPC uses Protocol Buffers for efficient binary serialisation and HTTP/2 for multiplexed streams; ideal for high-throughput internal service calls where JSON overhead matters.
- ✓gRPC uses HTTP/2 for multiplexed connections — multiple simultaneous RPCs over one TCP socket, unlike HTTP/1.1.
- ✓Protobuf binary encoding is ~5–10× smaller than JSON for the same data and significantly faster to serialise/deserialise.
- ✓Always set withDeadlineAfter() on client stubs — gRPC does not have default timeouts.
- ✓gRPC status codes (UNAVAILABLE, DEADLINE_EXCEEDED, NOT_FOUND) map to HTTP status codes differently; handle them explicitly.
- ✓gRPC-Web is required for browser clients; native gRPC cannot be called directly from browsers due to HTTP/2 trailers.
- ✓Use server streaming for pushing large result sets; bidirectional streaming for real-time collaborative features.
// order_service.proto
syntax = "proto3";
package com.example.grpc;
option java_multiple_files = true;
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc StreamOrders (StreamOrdersRequest) returns (stream OrderResponse);
}
message CreateOrderRequest {
string customer_id = 1;
double amount = 2;
repeated OrderItem items = 3;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
}
message OrderResponse {
string order_id = 1;
string status = 2;
}
message GetOrderRequest { string order_id = 1; }
message StreamOrdersRequest { string customer_id = 1; }
# pom.xml
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>GraphQL as an API Layer
GraphQL aggregates data from multiple microservices in a single query, reducing over-fetching and under-fetching; ideal for flexible client-driven data requirements.
- ✓GraphQL eliminates over-fetching (too many fields) and under-fetching (too many round trips)
- ✓Spring for GraphQL uses annotated controllers + schema-first .graphqls files
- ✓DataLoader batches N resolver calls into one bulk fetch to prevent the N+1 problem
- ✓Subscriptions over WebSocket enable real-time data push from GraphQL resolvers
- ✓Apollo Federation lets each microservice own its schema slice; the gateway composes them
- ✓GraphQL is not always better than REST — use it when clients have highly variable data needs
# schema.graphqls
type Query {
order(id: ID!): Order
}
type Order {
id: ID!
status: String!
customer: Customer # resolved by CustomerService
items: [OrderItem!]! # resolved by ProductService
}
@Controller
public class OrderController {
@QueryMapping
public Order order(@Argument Long id) {
return orderService.findById(id);
}
@SchemaMapping(typeName = "Order", field = "customer")
public Customer customer(Order order) {
return customerClient.findById(order.getCustomerId()); // HTTP / gRPC call
}
}API Versioning Strategies
Version APIs via URI path (/v1/), query parameter, or Accept header; semantic versioning and backward-compatible changes minimise downstream breakage.
- ✓URI versioning (/v1/) is most visible, cacheable, and widely used for public APIs
- ✓Header versioning keeps URIs clean but complicates caching and browser testing
- ✓Best strategy: additive-only changes (no version bump) + explicit versioning only for breaking changes
- ✓Breaking changes: remove/rename field, change type, make optional required, change status codes
- ✓Backward-compatible: add optional fields, new endpoints, make required fields optional
- ✓Pact (Consumer-Driven Contract Testing) catches breaking changes in CI before production
// Separate controllers per version
@RestController
@RequestMapping("/api/v1/orders")
class OrderControllerV1 {
@GetMapping("/{id}")
OrderResponseV1 getOrder(@PathVariable Long id) {
return orderService.findV1(id);
}
}
@RestController
@RequestMapping("/api/v2/orders")
class OrderControllerV2 {
@GetMapping("/{id}")
OrderResponseV2 getOrder(@PathVariable Long id) {
return orderService.findV2(id); // new response shape
}
}
// Or use a single controller with version routing
@RestController
@RequestMapping("/api/{version}/orders")
class OrderController {
@GetMapping("/{id}")
ResponseEntity<?> getOrder(
@PathVariable String version,
@PathVariable Long id) {
return switch (version) {
case "v1" -> ResponseEntity.ok(orderService.findV1(id));
case "v2" -> ResponseEntity.ok(orderService.findV2(id));
default -> ResponseEntity.notFound().build();
};
}
}