*Microservices

September 15, 2025

Why (when) to use them

  • Independent deployability and scaling per domain.
  • Smaller blast radius and clearer ownership when boundaries are correct.

Real costs

  • Network and data consistency complexity replaces in‑process calls.
  • Distributed observability, API versioning, and cross‑team coordination.

Practices that work

  • Design around DDD bounded contexts; each service owns its data.
  • Prefer asynchronous communication (events/queues) for workflows; use sync only for critical reads.
  • Backward‑compatible contracts and explicit versioning.
  • End‑to‑end tracing, correlation IDs, per‑service SLOs and budgets.

Data and consistency

  • Avoid cross‑service transactions; use the outbox pattern and idempotent consumers.
  • Sagas for long‑running workflows; design compensations explicitly.
  • Keep aggregates small; expose read models via materialized views.

Anti‑patterns

  • Microservices without automation (CI/CD, infra as code, service templates).
  • Sharing a single database across services → tight coupling and lock‑step deploys.

Start modular, earn your split, and move state out of processes before slicing services.

Migration playbook

  1. Strangle the monolith at the edge (routing/anti‑corruption layer).
  2. Carve off a low‑risk bounded context with its own data store.
  3. Introduce contracts and CDC; backfill read models.
  4. Measure SLOs and error budgets before/after; iterate.

Code: outbox pattern (SQL + worker)

-- language-sql
-- In the same transaction as a domain change, write an outbox row
BEGIN;
  UPDATE orders SET status = 'PAID' WHERE id = $1;
  INSERT INTO outbox(id, aggregate_id, type, payload, created_at)
  VALUES (gen_random_uuid(), $1, 'OrderPaid', to_jsonb($2::json), NOW());
COMMIT;
// language-typescript
// Worker polls outbox, publishes to a broker, marks as processed idempotently
type OutboxEvent = { id: string; type: string; payload: unknown }

async function handleOutbox(batch: OutboxEvent[]) {
  for (const evt of batch) {
    const already = await redis.setnx(`outbox:${evt.id}`, 1)
    if (!already) continue // idempotent
    await broker.publish(evt.type, evt.payload)
    await db.query('DELETE FROM outbox WHERE id = $1', [evt.id])
  }
}

Analogy

Microservices are like a team of small restaurants in a food court. Each shop specializes (bounded context) and has its own kitchen (database). The hallway (gateway/mesh) organizes orders and traffic. Sharing one kitchen leads to chaos.

Example scenario

  • Start with a modular monolith. Extract the “billing” context when it stresses team boundaries. Give it an outbox and a queue; build read models in “orders” via CDC.

FAQ

  • “How big should a service be?” Small enough to be owned by one team, big enough to avoid chatty cross‑service calls.
  • “Do I need a mesh?” Not initially. Add it once you need mTLS, traffic shaping, and consistent telemetry at scale.

Try it (trace budget)

Define a target: 95% of checkout traces < 700ms with < 3 cross‑service hops. Alert if exceeded.