Fundamentals — Cheat Sheet
Microservices · 7 topics. Download the PDF or the Instagram carousel and share it.
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.
// 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 independently12-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).
# 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 / SecretService 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.
// 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-serviceDomain-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.
// 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); }
}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.
// ── 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 revenueAggregates & 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.
// 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);
}
}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.
// 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 { ... }