Cheat SheetsMicroservicesFundamentals

Fundamentals — Cheat Sheet

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

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

Monolith vs Microservices

Monoliths are single deployable units; microservices split the system into independently deployable services, each owning its data and a well-defined API boundary.

  • Monolith: single deployment, shared DB, simple dev — becomes a bottleneck at team/scale growth.
  • Microservices: independently deployable, each owns data — high operational complexity.
  • Network calls replace method calls — introduce latency, partial failure, and eventual consistency.
  • Microservices require service discovery, distributed tracing, circuit breakers, and saga patterns.
  • Start with a modular monolith and extract services when pain demands it (Strangler Fig).
  • Team topology matters: Conway's Law says software structure mirrors communication structure.
Java — monolith structure pros/cons
// Monolith — all modules in one Spring Boot application
@SpringBootApplication
public class ShopApp { ... }

// Modules co-located in the same JVM:
// com.shop.orders    → OrderService, OrderController
// com.shop.inventory → InventoryService, InventoryController
// com.shop.billing   → BillingService, InvoiceController

// Single database — all modules query the same schema
@Service
public class OrderService {
    // Direct method call to InventoryService — in-process, no network
    public Order placeOrder(PlaceOrderRequest req) {
        inventoryService.reserve(req.getItems());   // same JVM
        billingService.charge(req.getCustomerId(), total);
        return orderRepository.save(order);
    }
}

// Pros:
// + Simple local development (one service to run)
// + ACID transactions across all modules
// + Easy debugging (single log stream, one JVM)

// Cons at scale:
// - Any change requires deploying the whole app
// - Team coordination friction — merge conflicts, slow releases
// - Can't scale individual components independently
2

12-Factor App Principles

The 12-Factor methodology (codebase, dependencies, config, backing services, build/release/run, processes, port binding, concurrency, disposability, dev/prod parity, logs, admin) defines best practices for cloud-native services.

  • Factor III (Config) is the most practically important: never hardcode environment-specific config — use env vars injected at runtime.
  • Factor VI (Processes): stateless services scale horizontally — no sticky sessions, no in-memory state shared between requests.
  • Factor IX (Disposability): set `server.shutdown=graceful` in Spring Boot to handle SIGTERM from Kubernetes gracefully.
  • Factor XI (Logs): write to stdout only — the platform aggregates logs; never write to log files inside the container.
  • Factor X (Dev/Prod Parity): use the same database engine locally as in production; H2 in dev with MySQL in prod hides SQL compatibility bugs.
  • The 12 factors predate Kubernetes but map directly to K8s primitives: ConfigMaps (III), ReplicaSets (VIII), SIGTERM handling (IX), and DaemonSet log shipping (XI).
YAML / Dockerfile — Factors III–V
# Factor III — Config from environment variables in Spring Boot
# application.yml
spring:
  datasource:
    url: ${DB_URL}           # from env var — never hardcode
    username: ${DB_USER}
    password: ${DB_PASS}
  kafka:
    bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS}

# Factor IV — Backing services as attached resources
# Swap local DB for prod by changing one env var:
# local:  DB_URL=jdbc:h2:mem:devdb
# prod:   DB_URL=jdbc:mysql://prod-db.example.com:3306/orders

# Factor V — Build → Release → Run with Docker
# Build: produces immutable JAR
FROM eclipse-temurin:21-jre AS base
COPY target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# Runtime config (Factor III) injected via K8s ConfigMap / Secret
3

Service Decomposition Strategies

Decompose by business capability, by subdomain (DDD), or by verb (action); wrong boundaries are the primary cause of microservices complexity and distributed monoliths.

  • Decompose by business capability, DDD bounded context, or use case — choose the strategy that fits your team structure.
  • Wrong service boundaries cause distributed monoliths (tight coupling) or nano-services (excessive overhead).
  • Each service must own its own data store — no shared databases.
  • Anti-Corruption Layers translate between bounded contexts to protect the internal model.
  • Conway's Law: team structure influences service boundaries; align them intentionally.
  • Use the strangler fig pattern to migrate a monolith incrementally without a big-bang rewrite.
Conceptual — business capability decomposition
// Business capabilities → services

// Capability: Order Management
// Service: order-service
// Owns: orders DB table, order lifecycle state machine

// Capability: Customer Management
// Service: customer-service
// Owns: customers DB, profile management

// Capability: Billing
// Service: billing-service
// Owns: invoices, payment records

// Anti-pattern — shared database across services:
// order-service → JOIN customers → violates service independence
// ✗ SELECT o.*, c.email FROM orders o JOIN customers c ON o.customer_id = c.id

// Correct — order-service calls customer-service API:
// ✓ GET /customers/{id} → returns CustomerDTO to order-service
4

Domain-Driven Design Basics

DDD aligns software structure with business domains; key building blocks include entities, value objects, aggregates, repositories, services, and domain events.

  • Entities have identity (ID); Value Objects are immutable and compared by value.
  • An Aggregate Root controls all mutations inside the aggregate and enforces invariants.
  • Repositories provide collection-like access to aggregate roots and hide persistence.
  • Domain Services hold stateless logic that spans multiple entities or aggregates.
  • Domain Events are facts that crossed a business boundary — publish them after persisting.
  • Bounded Context = explicit model boundary with its own Ubiquitous Language.
Java — Entity, Value Object, Aggregate Root
// Entity — has an ID, mutable over time
@Entity
public class Order {
    @Id
    private OrderId id;           // strong type for ID
    private CustomerId customerId;
    private OrderStatus status;
    private List<OrderLine> lines;

    // Business method enforcing invariant
    public void addLine(OrderLine line) {
        if (status != OrderStatus.DRAFT)
            throw new IllegalStateException("Cannot modify a confirmed order");
        lines.add(line);
    }
}

// Value Object — immutable, compared by value
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        if (amount.signum() < 0) throw new IllegalArgumentException("Negative money");
    }
    public Money add(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
        return new Money(amount.add(other.amount), currency);
    }
}

// OrderLine is part of the Order aggregate — accessed only via Order root
public record OrderLine(ProductId productId, int quantity, Money unitPrice) {
    public Money lineTotal() { return unitPrice.multiply(quantity); }
}
5

Bounded Contexts

A bounded context defines the boundary within which a particular domain model is valid and consistent; each microservice ideally maps to one bounded context.

  • A bounded context defines a boundary within which a domain model is valid; same business term can mean different things across contexts.
  • Each microservice should ideally map to exactly one bounded context — it owns its data and exposes a stable API.
  • Use an Anti-Corruption Layer (ACL) to translate external models into your local domain model — prevents coupling to other contexts.
  • Context map patterns: Shared Kernel (risky coupling), Customer/Supplier, ACL (isolation), Published Language (stable public schema).
  • Distributed monolith: services that share a database or require synchronous calls for every operation — worst of both worlds.
  • Start with fewer, larger contexts and split when a context grows too complex — wrong splits are costlier than delayed splits.
Java — Same Concept, Different Contexts
// ── Order context — cares about fulfilment ──────────────────────
package com.example.fulfilment.domain;

@Entity
public class Order {
    private String orderId;
    private String warehouseId;       // fulfilment-specific
    private List<PickItem> pickList;  // fulfilment-specific
    private ShippingLabel label;      // fulfilment-specific
    private FulfilmentStatus status;  // PICKING, PACKED, SHIPPED
}

// ── Finance context — cares about billing ────────────────────────
package com.example.finance.domain;

@Entity
public class Order {
    private String orderId;
    private BigDecimal   netAmount;        // finance-specific
    private BigDecimal   taxAmount;        // finance-specific
    private String       invoiceId;        // finance-specific
    private RevenueStatus revenueStatus;   // DEFERRED, RECOGNISED
}

// Same concept, different bounded contexts → different models
// Communication: Fulfilment publishes OrderShippedEvent
//                Finance subscribes and recognises revenue
6

Aggregates & Entities in DDD

An aggregate is a cluster of domain objects treated as a single unit; the aggregate root enforces invariants and is the only object that external code holds references to.

  • Aggregate Root is the single entry point for all mutations within the cluster.
  • External code holds only the Aggregate Root's ID — never direct references to internal entities.
  • All invariants are enforced inside Root methods before state changes are applied.
  • Keep aggregates small; use Domain Events + eventual consistency between aggregates.
  • One Repository per Aggregate Root — never repository methods for internal entities.
  • Reference other aggregates by ID to avoid loading unrelated data on every operation.
Java — Aggregate Root with invariant enforcement
// Order aggregate — Order is the root; OrderLine is an internal entity
@Entity
@Table(name = "orders")
public class Order {   // Aggregate Root

    @Id
    private OrderId id;
    private CustomerId customerId;   // reference by ID (not the Customer object)
    private OrderStatus status;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLine> lines = new ArrayList<>();

    private Money total;

    // All mutations go through root methods
    public void addLine(ProductId productId, int qty, Money price) {
        if (status != OrderStatus.DRAFT)
            throw new OrderNotEditableException(id);
        if (lines.size() >= 50)
            throw new TooManyLinesException(id);

        lines.add(new OrderLine(productId, qty, price));
        this.total = recalculateTotal();
    }

    public void place() {
        if (lines.isEmpty())
            throw new EmptyOrderException(id);
        this.status = OrderStatus.PLACED;
        registerEvent(new OrderPlaced(id, customerId, total));
    }

    private Money recalculateTotal() {
        return lines.stream()
            .map(OrderLine::lineTotal)
            .reduce(Money.ZERO, Money::add);
    }
}
7

Microservices Design Patterns Overview

Core patterns include API Gateway, Service Registry, Circuit Breaker, Saga, CQRS, Event Sourcing, Sidecar, and Strangler Fig — each solves a specific distributed-systems challenge.

  • API Gateway: single entry point for routing, auth, rate limiting, SSL termination.
  • Service Registry (Eureka/Consul): services register on start; clients look up instances dynamically.
  • Circuit Breaker: fail fast when a downstream is unhealthy; return fallback response.
  • Saga: distributed transaction via compensating local transactions (choreography or orchestration).
  • CQRS: separate write (command) and read (query) models for independent optimisation.
  • Outbox Pattern: atomic message publishing by writing events to a DB table in the same transaction.
YAML + Java — API Gateway + Service Registry
// Pattern 1: API Gateway
// Single entry point for all client requests
// Responsibilities: auth, rate limiting, routing, SSL termination, CORS

// Spring Cloud Gateway config
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service        # lb:// = load-balanced via Eureka
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1
            - name: CircuitBreaker
              args: { name: orderCB, fallbackUri: forward:/fallback }
            - name: RequestRateLimiter
              args: { redis-rate-limiter.replenishRate: 100, redis-rate-limiter.burstCapacity: 200 }

// Pattern 2: Service Registry (Eureka)
// Services register on startup; gateway/clients look up instances
@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServerApp { ... }

@EnableDiscoveryClient  // on each microservice
@SpringBootApplication
public class OrderServiceApp { ... }
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/microservices