Cheat SheetsMicroservicesPatterns

Patterns — Cheat Sheet

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

Cheat Sheet · AiCanCode.org
Patterns
Microservices6 topicsQuick revision reference
1

Strangler Fig Pattern

Incrementally migrate a monolith by routing individual features to new microservices behind an API gateway until the monolith is completely replaced.

  • Strangler Fig: add a gateway, route features to new services incrementally, shrink the monolith
  • No big-bang cutover — both monolith and services run in parallel; rollback is a route toggle
  • Use Debezium CDC or dual-write to migrate data ownership from monolith to new service DB
  • A distributed monolith (shared DB + synchronous coupling) is worse than the original monolith
  • Each extracted service must be independently deployable, scalable, and own its data
  • Extract high-ROI, low-coupling features first (e.g., notifications, reporting)
Spring Cloud Gateway — strangler routing phases
# Spring Cloud Gateway — strangler routing config
# Phase 1: everything goes to monolith
spring:
  cloud:
    gateway:
      routes:
        - id: monolith
          uri: http://monolith-service
          predicates: [Path=/**]

# Phase 2: order endpoints → new order-service, rest → monolith
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://order-service
          predicates: [Path=/api/orders/**, /api/order-items/**]
          order: 1

        - id: monolith-fallback
          uri: http://monolith-service
          predicates: [Path=/**]
          order: 100    # lower precedence
2

Idempotency in APIs

An idempotent operation produces the same result when called multiple times; implement via idempotency keys stored in Redis or a DB to deduplicate retries.

  • HTTP GET and PUT are naturally idempotent; POST, PATCH, and DELETE need explicit deduplication logic.
  • Idempotency keys must be client-generated (UUID v4) and scoped to a single logical operation, not per-retry.
  • Redis SET NX with TTL is the fastest implementation; a DB table is better when you need audit history.
  • The check-and-store must be atomic — race conditions between two identical concurrent requests can cause double-processing.
  • Store the full response body, not just a "done" flag, so replays return identical HTTP status and payload.
  • Set a reasonable TTL (24h–7d) to bound storage growth; communicate the window to API consumers.
Java — Spring MVC interceptor
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {

    private final StringRedisTemplate redis;

    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse res, Object handler) throws Exception {
        String key = req.getHeader("Idempotency-Key");
        if (key == null) return true;           // key optional for GET

        String cached = redis.opsForValue().get("idem:" + key);
        if (cached != null) {
            res.setStatus(200);
            res.setContentType("application/json");
            res.getWriter().write(cached);
            return false;                       // short-circuit, reply cached
        }
        return true;
    }

    // Call this from controller advice after response committed
    public void cacheResponse(String key, String responseBody) {
        redis.opsForValue().set("idem:" + key, responseBody,
                Duration.ofHours(24));
    }
}
3

Feature Flags in Microservices

Feature flags decouple deployment from release; toggle features per environment or user segment without redeploying, enabling dark launches and A/B tests.

  • Feature flags decouple deployment (code merged and deployed) from release (feature visible to users)
  • OpenFeature provides a vendor-neutral SDK — swap providers (LaunchDarkly, Unleash, Flagsmith) without code changes
  • Evaluation context (userId, country, plan) enables targeting: specific user segments get different flag values
  • Kill switches must be pre-tested and accessible to on-call engineers — not just developers — for true operational value
  • Gradual rollout (1% → 10% → 50% → 100%) enables safe production validation before full release
  • Remove flags from code after full rollout — dead flags become technical debt and confuse new developers
YAML + Java — property-based feature flags with ConfigMap reload
// application.yml — feature flags as properties
features:
  new-checkout-flow: true
  experimental-pricing: false
  max-order-size-check: true

// Configuration properties class
@ConfigurationProperties(prefix = "features")
@Component
public class FeatureFlags {
    private boolean newCheckoutFlow;
    private boolean experimentalPricing;
    private boolean maxOrderSizeCheck;
    // getters/setters or use @ConstructorBinding record
}

// Usage in service
@Service
public class CheckoutService {

    private final FeatureFlags flags;

    public CheckoutResult checkout(Cart cart, User user) {
        if (flags.isNewCheckoutFlow()) {
            return newCheckoutFlowService.process(cart, user);
        }
        return legacyCheckoutService.process(cart, user);
    }
}

// Toggle without redeployment via K8s ConfigMap reload
# kubectl patch configmap app-config
# --patch '{"data":{"features.new-checkout-flow":"true"}}'
# (requires spring.config.import=kubernetes: + @RefreshScope or restart)
4

Backward Compatibility & Contract Testing

Consumer-Driven Contract Testing (Pact) verifies that provider APIs remain compatible with consumer expectations, catching breaking changes before they reach production.

  • Consumer-Driven Contract Testing catches breaking API changes in provider CI before they reach production
  • Pact contracts define minimum required response shape — type-matching (not value-matching) makes contracts resilient
  • Provider state ("given()" in Pact) sets up test data to match each consumer scenario — must be deterministic
  • Publish verification results to Pact Broker with git commit/branch — enables "can-i-deploy" checks in deployment pipelines
  • Avro/JSON Schema BACKWARD compatibility: add optional fields; NOT backward compatible: remove/rename required fields
  • FULL compatibility (add optional fields with defaults) is the safest evolution strategy for bidirectional compatibility
Java — Pact consumer contract definition and mock server test
// Consumer side — define contract in a consumer test
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "inventory-service")
public class InventoryClientContractTest {

    @Pact(consumer = "order-service")
    public RequestResponsePact stockLookupPact(PactDslWithProvider builder) {
        return builder
            .given("product 101 exists with stock 50")
            .uponReceiving("a request for product stock")
                .path("/stock/101")
                .method("GET")
            .willRespondWith()
                .status(200)
                .body(new PactDslJsonBody()
                    .integerType("productId", 101)    // type-only matching (flexible)
                    .integerType("quantity", 50)
                    .stringType("unit", "units"))
            .toPact();
    }

    @Test
    @PactTestFor(pactMethod = "stockLookupPact")
    public void testStockLookup(MockServer mockServer) {
        InventoryClient client = new InventoryClient(mockServer.getUrl());
        StockResponse response = client.getStock(101L);
        assertThat(response.getQuantity()).isGreaterThanOrEqualTo(0);
        // Pact generates a contract JSON file — publish to Pact Broker
    }
}
5

Graceful Shutdown

On SIGTERM, stop accepting new requests, drain in-flight requests within a configurable timeout, then close connections — Spring Boot handles this via server.shutdown=graceful.

  • server.shutdown=graceful stops accepting new requests and waits for in-flight requests to finish
  • spring.lifecycle.timeout-per-shutdown-phase controls the drain window (default: 30s)
  • Kubernetes preStop sleep gives the load balancer time to deregister the pod before SIGTERM
  • terminationGracePeriodSeconds = preStop time + drain window + buffer (set to their sum)
  • Kafka consumers drain via SmartLifecycle.stop() — called automatically during context shutdown
  • Readiness probe /actuator/health/readiness goes DOWN on shutdown — prevents new traffic routing
Spring Boot — graceful shutdown + Kubernetes config
# application.properties
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s   # wait up to 20s for in-flight HTTP requests

# --- Kubernetes Deployment YAML ---
spec:
  containers:
    - name: order-service
      lifecycle:
        preStop:
          exec:
            command: ["sh", "-c", "sleep 5"]    # wait 5s after deregister before SIGTERM
  terminationGracePeriodSeconds: 30              # total time before SIGKILL
  # Timeline:
  # t=0:  SIGTERM sent + preStop hook runs (5s sleep)
  # t=5:  Spring receives SIGTERM → stops accepting new requests
  # t=25: Drain window closes (20s)
  # t=30: SIGKILL if process still running
6

Microservices Anti-Patterns

Common pitfalls include distributed monolith (tight coupling), chatty services, shared databases, synchronous call chains, and under-invested observability infrastructure.

  • Distributed Monolith: services that cannot be independently deployed due to shared DB, circular deps, or shared domain models
  • Shared database: the most common coupling source — each service must own its data; share via API or events
  • Chatty services: N serial round trips per user request — solve with BFF aggregation or batch APIs
  • Deep synchronous chains (A→B→C→D): failure rate and latency multiply — break with async events for non-critical paths
  • Nano-services: overhead without benefit — merge if it cannot be independently developed and owned by a small team
  • Under-invested observability: the #1 operational pain — structured logs, RED metrics, tracing, and alerting are non-negotiable
Java — shared database anti-pattern vs database-per-service solution
// ANTI-PATTERN: Shared Database — OrderService and InventoryService
// both access the same "shop" database directly

// OrderService
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // queries shop.orders table
}

// InventoryService — WRONG: directly queries shop.orders to check order status
@Repository
public interface OrderQueryRepository extends JpaRepository<Order, Long> {
    // BAD: InventoryService depends on OrderService's database schema
    @Query("SELECT o FROM Order o WHERE o.status = 'PLACED'")
    List<Order> findPlacedOrders();
}

// SOLUTION: Each service owns its data; cross-service data via API or events
// OrderService exposes: GET /orders?status=PLACED
// InventoryService calls OrderService's API (or subscribes to order-placed events)

// Or better: InventoryService maintains its own read model
// populated by consuming "order-placed" Kafka events
@KafkaListener(topics = "order-placed")
public void onOrderPlaced(OrderPlacedEvent event) {
    // InventoryService maintains its own copy of relevant order data
    pendingOrderRepository.save(new PendingOrder(event.getOrderId(), event.getItems()));
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/microservices