Cheat SheetsMicroservicesCommunication

Communication — Cheat Sheet

Microservices · 7 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Communication
Microservices7 topicsQuick revision reference
1

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.
Java — OpenFeign client with fallback
// 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
}
2

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
Microservices — synchronous HTTP with circuit breaker
// 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)
3

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.
Java + Properties — Spring Cloud Stream
// 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);
    }
}
4

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.
Java — Publishing a Domain Event
// 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;
    }
}
5

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.
Protobuf + XML — .proto definition + Spring dependency
// 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>
6

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
Java — Spring for GraphQL schema + resolvers
# 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
    }
}
7

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
Spring Boot — URI path versioning
// 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();
        };
    }
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/microservices