Skip to content

10.8.3 — Microservices: What They Genuinely Are, and the Patterns Around Them

The most-hyped and least-defined architecture in the industry. This page answers the Curiosity bank's question directly — what is a microservice actually and genuinely — then covers how to draw boundaries (the decision that determines whether the architecture helps or hurts), service discovery, API gateway, BFF, and sidecar. Each is 9.4's pattern discipline at network scale: tension → structure → cost → when not to.

1. What a microservice genuinely is

DrillCuriosity #24 (verbatim): What is micro-service actually and genuinely? How build my own Fast API micro-service? Is RabbitMQ is a go to for locally built microservices? Can we do it for Nodejs as well? How many design patterns are involved with this architecture?

Genuinely: a microservice is an independently deployable unit that owns its data and is operated by one team. That's the whole definition, and every word in it is doing work. Independently deployable — it ships without coordinating a release with anything else (the actual benefit; if two services must deploy together, they're one service with a network call between them, and you have all the costs and none of the gain). Owns its data — no other service reads or writes its database; access is through its API or its events (10.4's state ownership). A shared database across "services" is a distributed monolith: the schema couples every deployment while the network adds failure modes. One team — the boundary matches an ownership boundary, so the coordination it removes is real coordination.

What it is not: small (a microservice can be tens of thousands of lines — "micro" refers to scope of responsibility, not line count); a technology (any language, any framework); or a prerequisite for scale (well-factored monoliths serve enormous traffic — 10.1).

Building one — the concrete part of the question. In FastAPI or Node/Express, "a microservice" is an ordinary application with four properties added: (1) its own datastore and schema; (2) an explicit API contract (OpenAPI — 9.6.4); (3) health/readiness endpoints, structured logs with trace propagation, and metrics (9.9.7, 10.10); (4) an independent deploy pipeline. The framework is the least interesting choice — FastAPI and Express are equally fine, and mixing them is normal.

Is RabbitMQ the go-to? For local and small-scale service-to-service messaging: yes, it's an excellent default — mature, easy to run in Docker, rich routing, per-message operations, good client libraries in both Python and Node (10.8.1's broker column). Choose Kafka instead when you need retention/replay, multiple independent consumer groups, or very high throughput; and for many internal calls, plain HTTP request/response is the right answer — messaging is for asynchronous work, not for every interaction. Yes, the same designs work in Node.js: amqplib for RabbitMQ, kafkajs for Kafka, and the patterns are identical because they're protocol-level, not language-level.

How many patterns are involved? The honest count: roughly a dozen that matter, and you already have most of them — communication (request/response, async messaging, streaming — [5.8], 10.8.1); resilience (timeout, retry with backoff, circuit breaker, bulkhead — 10.9); data (database-per-service, saga, outbox, CQRS, event sourcing — 10.8.4); infrastructure (service discovery, API gateway, BFF, sidecar — this page); and operational (health checks, distributed tracing, centralized config — 10.10). Not a new catalog — Part 9's patterns, applied where the arrows cross machines.

2. Boundaries: the decision that decides everything

A microservice architecture is only as good as its boundaries, and the failure mode is universal: services split by technical layer (a "database service," a "validation service") or by entity (a service per table) rather than by business capability. The heuristics that work:

  • Split by capability, not by noun. "Order management" (which owns orders, their lifecycle, and their rules) is a capability; "Order table CRUD" is not. Domain-Driven Design's bounded context is the formal version: a boundary within which a model has one consistent meaning — and the tell that you've found one is that the vocabulary changes at the edge (a "customer" in billing is not the same object as a "customer" in support).
  • Follow team ownership (Conway's Law). Systems mirror the communication structure of the organizations that build them, so a boundary that cuts across a team produces constant cross-team coordination — the very thing the architecture was meant to remove.
  • Test with the transaction question. If a single user action must atomically change data in two services, the boundary is probably wrong: you've turned a local transaction into a distributed one, and now need sagas (10.8.4) for something a monolith did with BEGIN. Boundaries should make the common operations local.
  • Test with the change question. If features routinely require simultaneous changes to three services, they aren't independently deployable and the split is costing you without paying.

The practical sequencing most successful organizations follow: start with a modular monolith (clear internal boundaries, one deployable — 9.9.6), let real usage reveal where the seams and scaling pressures actually are, then extract the pieces that have a named reason (10.1's three legitimate reasons). Extracting a well-defined module is a week; un-splitting a wrong boundary is a quarter.

3. Service discovery

With services scaling elastically, "where is the payments service?" cannot be answered by a hardcoded address. Service discovery solves it in one of three shapes: What is service discovery and how does it work? [EQ-492b]

  • Client-side discovery — the caller queries a registry (Consul, Eureka, etcd) and load-balances itself. Fewer hops, but every client needs the logic and the registry client library.
  • Server-side discovery — the caller addresses a stable endpoint (a load balancer or Kubernetes Service name) and the infrastructure routes. Simpler clients; an extra hop.
  • DNS-based — the platform maps service names to healthy endpoints (Kubernetes' cluster DNS is this, over a virtual IP). The default in container platforms, and usually the right answer because it's language-agnostic and requires nothing in your code.

The registry needs health awareness (only healthy instances are returned — 9.7.6's health state machine, at fleet scale) and its own availability story (it's a dependency of every call — usually consensus-backed, 10.7.2, with client-side caching so a registry blip doesn't take down everything).

4. Gateway, BFF, sidecar

mobilewebpartnerBFF mobilethin payloadsBFF webrich aggregatesAPI GATEWAYauthn · rate limitrouting · TLS(cross-cutting only)orderssidecarpaymentssidecarinventorysidecarGateway = one door for cross-cutting concerns · BFF = one door per client shape · Sidecar = per-instance network behavior
Figure 1 — The three edge patterns. A gateway centralizes cross-cutting concerns for all traffic; BFFs shape responses per client type; sidecars move retries, mTLS, and telemetry out of application code into a per-instance proxy.

API gateway — a single entry point for external clients that handles cross-cutting concerns: TLS termination, authentication, rate limiting, routing, request/response transformation, and sometimes response aggregation. It's 9.4.9's Facade with an IP address — clients see one API instead of twelve, and services stop each implementing auth. The failure mode to avoid: the gateway accumulating business logic until it becomes a distributed monolith's brain — every feature requires a gateway change, and the team owning it becomes the bottleneck. Rule: cross-cutting concerns only; no domain logic.

Backend for Frontend (BFF) — a per-client-type backend (mobile BFF, web BFF, partner API) that aggregates and shapes data for that client. The tension it resolves: one API cannot serve a mobile app (few round trips, minimal payloads, battery-aware) and a web dashboard (rich aggregates) and a partner integration (stability over years) without becoming a compromise that serves none well. Each BFF is owned by the client team, evolves at their pace, and does aggregation and shaping — not business rules (9.9.6's layering: BFFs are presentation layers that happen to run on a server). Cost: another deployable per client type, and a real risk of logic duplication across BFFs.

Sidecar — a helper process deployed alongside each service instance (same pod/host) that handles network concerns outside the application: mTLS, retries, circuit breaking, load balancing, tracing, metrics (the common sidecar program is Envoy; when you add a central controller that configures every sidecar at once, the whole arrangement is called a service mesh, and Istio and Linkerd are the two best-known ones). The appeal: policy applied uniformly across languages without library upgrades in every service — genuinely valuable in polyglot fleets. The costs are real: extra latency per hop, memory per instance, an additional failure mode, and substantial operational complexity (9.4.1's pattern-fever test applies at infrastructure scale — a service mesh for eight services is usually more complexity than it removes).

5. The expert lens

Microservices trade one hard problem for a different one. They convert code coupling (which compilers and tests help with) into network coupling (which needs timeouts, retries, contracts, and tracing). That trade pays when the pain being removed — deployment contention, scaling one component, team autonomy — is real and named; it's a loss otherwise, which is why the honest architectural default is a modular monolith with extraction on evidence (10.1).

The distributed monolith is the worst outcome, and it's easy to build accidentally. Its signatures: services sharing a database; releases that must be coordinated; a change to one entity touching four repos; synchronous call chains three or four deep (where availability multiplies and latency adds). If a design has these, splitting has bought network failure modes without buying independence — and the fix is usually to merge services back and re-split along capability lines (section 2).

Every edge pattern is a Part 9 pattern with an IP address. Gateway = Facade; BFF = an adapter per client; sidecar = Proxy/Decorator; discovery = a registry + Factory. Recognizing this keeps you from treating infrastructure as magic: the same questions apply — what tension does it resolve, what does it cost, and what happens when it fails (9.4.1).

Next: 10.8.4 — the data patterns that make service-owned databases workable: saga, outbox, CQRS, and event sourcing.

Recall

  • A microservice is: independently deployable + owns its data + one team. Not "small," not a technology, not required for scale. Shared database or coordinated releases = distributed monolith (all costs, no benefit). Building one = ordinary app + own datastore + explicit contract + health/logs/metrics/tracing + independent pipeline.
  • Boundaries decide everything: split by business capability / bounded context (vocabulary changes at the edge), follow team ownership (Conway), and test with the transaction question (must one action atomically touch two services? wrong boundary) and the change question (do features routinely touch three services?). Sequence: modular monolith → extract on evidence.
  • Service discovery: client-side (registry + client LB), server-side (stable endpoint, infra routes), or DNS-based (Kubernetes default, language-agnostic). Needs health awareness and its own availability story (consensus-backed + client caching).
  • API gateway = Facade with an IP: TLS, authn, rate limits, routing — cross-cutting only, never domain logic. BFF = one backend per client shape (mobile/web/partner), owned by the client team, aggregation and shaping only. Sidecar = per-instance proxy for mTLS/retries/breaking/tracing (mesh = sidecars + control plane) — uniform polyglot policy, at the cost of latency, memory, complexity.
  • RabbitMQ vs Kafka vs HTTP for services: Rabbit is a fine default for local/small async work; Kafka when you need retention/replay/multiple groups/throughput; plain HTTP for synchronous interactions — messaging is for async work, not for everything. Same designs in Python and Node.
  • Lens: microservices swap code coupling for network coupling — pay only for a named pain; the distributed monolith is the common accidental outcome (shared DB, coordinated releases, deep sync chains); every edge pattern is a Part 9 pattern with an IP address.

Self-test: Give the three-clause definition and what each prevents. Name four boundary heuristics. Which discovery model needs nothing in your code? What must never live in a gateway? State the sidecar's benefit and three costs.

Quiz Bank

FoundationalDefine a microservice precisely, and name the anti-pattern that violates the definition.

Independently deployable, owns its data, operated by one team. Independently deployable is the benefit that justifies everything else — if shipping a change requires coordinating releases with another service, you have the costs of distribution without its payoff.

Owns its data means no other service touches its database directly; integration happens through its API or its published events (10.4's single-owner rule). One team aligns the technical boundary with an ownership boundary, so the coordination removed is real. What it is not: "small" (scope of responsibility, not line count — a microservice can be large), a specific technology, or a prerequisite for scale.

The anti-pattern: the distributed monolith — services that share a database, must be released together, or form deep synchronous call chains. It has every distributed cost (network failures, latency, tracing complexity, eventual consistency) and none of the benefits (no independent deploy, no isolated failure, no team autonomy). Its diagnostic signatures are worth memorizing: a shared schema, a "release train," a feature that touches four repos, and availability that multiplies down a call chain rather than degrading gracefully.

FoundationalHow do you decide where service boundaries go?

Four tests, applied together. (1) Business capability / bounded context — a boundary should enclose a coherent capability with a single consistent model; the reliable tell is that the vocabulary changes at the edge ("customer" means something different in billing than in support), which signals a genuine context boundary rather than an arbitrary cut.

(2) Team ownership (Conway's Law) — systems mirror organizational communication, so a boundary crossing a team creates permanent coordination overhead, defeating the purpose. (3) The transaction test — if one user action must atomically modify data in two services, the boundary is probably wrong: you've converted a local BEGIN…COMMIT into a saga with compensations (10.8.4); good boundaries make common operations local.

(4) The change test — if typical features require simultaneous changes across three services, they aren't independently deployable, so the split costs without paying. Anti-heuristics to avoid: splitting by technical layer (an "auth service," a "database service," a "validation service" — these are libraries, not services) and splitting by table (a service per entity guarantees cross-service transactions).

The sequencing that avoids expensive mistakes: build a modular monolith with enforced internal boundaries (9.9.6), observe where scaling and change pressure actually concentrate, then extract with a named reason (10.1) — extraction is cheap, un-splitting is not.

AppliedExplain API gateway vs BFF: what each solves, and what must not go into them.

API gateway — a single entry point for all external traffic, handling cross-cutting concerns: TLS termination, authentication (validating tokens once, passing identity inward), rate limiting, routing, request/response transformation, and observability. It's a Facade with an IP address (9.4.9): clients see one API rather than twelve hostnames, and services stop reimplementing auth and limits.

BFF (backend for frontend) — one backend per client type, resolving a different tension: a mobile app needs few round trips and small payloads, a web dashboard wants rich aggregates, and a partner API needs multi-year stability; a single API serving all three is a compromise that serves none well. Each BFF aggregates and shapes for its client, is owned by that client's team, and evolves at that team's pace.

What must not go in either: business logic. A gateway that accumulates domain rules becomes a distributed monolith's brain — every feature requires a gateway change and the gateway team becomes the organization's bottleneck; a BFF that owns rules duplicates them across client types and drifts. Both are presentation and cross-cutting layers (9.9.6's layering discipline): domain logic stays in the services that own the data. They compose naturally — clients → BFF (shaping) → gateway (cross-cutting) → services, or gateway first with BFFs behind it, depending on where you want TLS and auth terminated.

InterviewWhat problem does a sidecar solve, and when is a service mesh not worth it?

A sidecar runs beside each service instance (same pod) and takes over network concerns: mutual TLS, retries with backoff, circuit breaking, client-side load balancing, timeouts, traffic shifting (canaries), and telemetry/tracing emission. The problem it solves is polyglot policy drift: without it, every service in every language must implement the same resilience and security behavior via libraries, and upgrading a retry policy means upgrading N libraries in M languages on their own schedules. With a mesh (sidecars + a control plane like Istio/Linkerd), policy is declared centrally and applied uniformly, and application code contains no networking logic.

When it's not worth it: small fleets (a handful of services, one or two languages — where a shared client library achieves the same at a fraction of the complexity), latency-sensitive paths (every hop now traverses two extra proxies), constrained environments (per-instance memory and CPU overhead is real), and teams without the operational capacity to run a mesh (its failure modes — certificate rotation, config propagation, proxy bugs — are subtle and land on whoever is on call). The 9.4.1 test applies: adopt it when the tension (polyglot, many services, uniform security/traffic policy, mTLS mandates) is demonstrated — not because the architecture diagram looks more modern with it.

StaffAn organization with 8 engineers has 26 microservices, a shared Postgres, and a weekly coordinated release. Diagnose, and lay out the remediation with sequencing.

Diagnosis: a distributed monolith with a staffing mismatch. Three independent problems. (1) Shared Postgres means no service owns its data — the schema is a global coupling point, so any migration is a fleet-wide event and no service can evolve its model independently (10.4). (2)

Weekly coordinated release proves nothing is independently deployable, which was the entire justification for splitting; they are paying network failure modes, tracing complexity, and 26 deploy pipelines for zero autonomy. (3) 8 engineers, 26 services ≈ 3+ services per engineer: on-call, dependency upgrades, CI maintenance, and cross-service debugging consume the capacity that should be building product — the coordination cost landed on the same humans it was meant to free.

Remediation, sequenced by risk-adjusted value. Phase 1 — stop the bleeding, don't re-architect: freeze new service creation; establish which services actually have distinct owners and scaling profiles (usually 3–6 of 26); introduce contract tests and tracing so the current system is diagnosable during the change (10.10).

Phase 2 — consolidate: merge services that always deploy together into single deployables, keeping their code as modules with enforced boundaries (9.9.6) — this is the fastest, safest win, and it's un-splitting, which teams resist emotionally and benefit from enormously; target something like 4–6 services aligned to capabilities, not entities (section 2).

Phase 3 — untangle data: for each surviving service, assign schema ownership; where two services share tables, either merge them (if the data is one capability) or give one ownership and the other an API/event feed, migrating with dual-write + backfill + verification (10.11).

Phase 4 — earn independence: per-service pipelines, per-service on-call, and the release train dissolves — measured by deploy frequency and lead time rather than by service count. The framing for leadership: service count is a cost, not an achievement; the target is the smallest number of independently deployable units that matches team ownership and named scaling needs — for 8 engineers that number is small, and getting there will make the team faster within a quarter.

Flashcards

FlashMicroservice, genuinely

Independently deployable + owns its data + one team. Not "small," not a technology. Shared DB or coordinated release = distributed monolith.

FlashBoundary tests

Business capability/bounded context (vocabulary changes) · team ownership (Conway) · transaction test (atomic across two = wrong) · change test (features touching three = wrong).

FlashDiscovery models

Client-side (registry + client LB) · server-side (stable endpoint) · DNS-based (K8s — nothing in your code). All need health awareness + caching.

FlashGateway vs BFF

Gateway = one door, cross-cutting only (TLS, authn, limits, routing). BFF = one backend per client shape (mobile/web/partner), aggregation + shaping. Neither holds domain logic.

FlashSidecar

Per-instance proxy: mTLS, retries, breaking, tracing — uniform policy across languages. Costs: latency, memory, failure mode, operational complexity. Not for small fleets.

FlashRabbitMQ / Kafka / HTTP

Rabbit: fine default for async tasks, rich routing, local dev. Kafka: retention, replay, many groups, throughput. HTTP: synchronous interactions — most calls.

Scenario Drill

DrillYou're asked to design a microservice architecture for a mid-sized insurance company: policy administration, claims, billing, document generation, and a customer portal. Four teams, on-prem plus cloud, strict audit requirements. Draw the boundaries, choose the communication styles per interaction, and name the three decisions you'd defer.

Boundaries by capability and vocabulary (section 2): Policy Administration (quotes, underwriting rules, policy lifecycle — the domain's core complexity and its own team), Claims (FNOL through settlement — a genuinely different model where a "policy" is a read-only reference, not an editable object: the vocabulary shift confirms the boundary), Billing (invoicing, payments, dunning — different change cadence, different compliance surface, and often a different vendor integration), and Documents (policy packets, claim letters — a technical capability, which normally would be a library rather than a service, but earns servicehood here because document generation is CPU-heavy and bursty, needing independent scaling — a named reason, 10.1). The customer portal is not a service: it's a BFF owned by the portal team, aggregating from the four services and shaping for web/mobile (section 4), with a second BFF later if the mobile app's needs diverge. Four teams, four capability services plus BFFs — the boundary count matches the org, deliberately.

Communication per interaction: portal reads → synchronous HTTP through the BFF (users are waiting; aggregation is a read concern). Policy → Claims ("is this policy in force on the loss date?") → synchronous HTTP with timeout + circuit breaker + a cached fallback (10.9), because claims cannot proceed without it but must degrade rather than fail hard.

Any state change worth auditing (policy issued, claim approved, invoice raised) → events on a durable log (10.8.1) published via the outbox (10.8.4) — which simultaneously satisfies the audit requirement (an immutable, ordered, replayable record of what happened, 9.7.29's ledger discipline at system scale) and decouples the consumers.

Document generationqueue (a command, executed once, retried with backoff, DLQ'd on poison input — the 10.8.1 tell), returning a 202 and a status resource so the portal can poll or subscribe. Billing → Payments provider → synchronous with idempotency keys (10.4).

On-prem/cloud split shapes one thing sharply: cross-boundary calls are slow and less reliable, so the split should follow the deployment reality — put chatty pairs on the same side, and make the cross-boundary interactions asynchronous and idempotent by preference.

Three decisions to defer, explicitly: (1) a service mesh — four services in a mixed environment don't yet justify it; revisit when mTLS is mandated fleet-wide or the service count passes ~10 (section 4); (2) CQRS/event-sourcing for policy — the audit trail is satisfied by the event log without restructuring the write model; revisit if temporal queries ("what did this policy look like on 3 March?") become a product requirement rather than an audit one (10.8.4); (3)

splitting Documents further (templating vs rendering vs archival) — premature until one of them has a distinct scaling or ownership story. Recording deferrals with their triggers is the same discipline as 9.9.6's omissions list, and it's what keeps an architecture from accreting services that nobody chose.