Skip to content

Architecture Styles

This page maps the major architectural styles used to structure a backend system: how components are decomposed, how they talk to each other, and how they are deployed and scaled. It is a level above API Design Concepts, which covers how one HTTP interface behaves — this page covers how the system behind that interface is shaped. Most real products combine several of these styles rather than picking exactly one; the goal here is to know what each style optimizes for, what it costs, and when to reach for it.

Abbreviations: SOA (Service-Oriented Architecture), ESB (Enterprise Service Bus), FaaS (Function as a Service), CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend), N-tier (multi-tier), SLA (Service Level Agreement), DR (Disaster Recovery), IaC (Infrastructure as Code).

A monolith ships the entire application — UI, business logic, and data access — as one deployable unit, usually against one database. This is the default starting point for most products: one codebase, one build, one deploy pipeline.

blockdiag

  • Simple to build and reason about — one codebase, one language, one set of tests, one deploy.
  • Easy transactions — a single database means ACID transactions across the whole write path, no distributed coordination.
  • Cheap to operate early — one thing to deploy, monitor, and scale; no service mesh or gateway needed.
  • Scales as a unit — a CPU-heavy report endpoint forces you to scale the whole app, not just that path.
  • Deploy risk grows with size — one bad change can take down everything; release cadence slows as teams collide on the same codebase.
  • Technology lock-in — hard to adopt a new language or datastore for just one feature.
Use whenPoor fit when
New product, small team, unproven domain boundariesMultiple teams need to ship independently at high frequency
Strong consistency across most operationsDifferent parts of the system have wildly different scaling profiles
Low operational budget (no platform team yet)Regulatory or org requirement to isolate blast radius per domain

Avoid

  • Treating “monolith” as an excuse to skip internal module boundaries — a monolith with no internal seams becomes a big ball of mud, which is what makes a later split to microservices painful.
  • Splitting into microservices before you understand your domain boundaries “because monoliths don’t scale” — most monoliths fail on organization, not load.

Layered architecture organizes code within a deployable (monolith or single service) into horizontal layers — typically presentation, business logic, and data access — where each layer only calls the layer below it. This is a code-organization pattern, not a deployment topology; it is usually the internal structure of a monolith or of one microservice.

blockdiag

Hexagonal / clean architecture is a variant that inverts the dependency: business logic sits at the center and depends on ports (interfaces), while presentation and data access are adapters plugged in from the outside — making the business logic testable without a real database or HTTP framework.

  • Clear separation of concerns — swap the database or the web framework without rewriting business rules.
  • Testable in isolation — business logic tested without spinning up HTTP or a real DB.
  • Layer leakage — under deadline pressure, teams reach straight from presentation to data access, and the layering becomes decorative.
  • Extra indirection for genuinely simple CRUD (Create, Read, Update, Delete) features — hexagonal ports/adapters can be overkill for a five-endpoint service.
Use whenPoor fit when
Business rules are non-trivial and must be unit-tested without infrastructureThe service is a thin pass-through to a database (layering adds no value)
You expect to swap a framework, ORM, or datastore laterTeam is small and the extra indirection slows delivery more than it helps

Avoid

  • Letting the presentation layer import data-access models directly — that’s the leak that eventually makes layers pointless.

Microservices split a system into independently deployable services, each owning its own data and business capability, communicating over the network (HTTP, gRPC, or async messaging). The unit of scaling, deployment, and team ownership is the service, not the app.

d2

  • Independent deploys — teams ship their service without coordinating a monolith-wide release.
  • Scale per bottleneck — scale the payments service without over-provisioning inventory.
  • Technology freedom per service — a service can use the language/datastore that fits its workload.
  • Distributed systems tax — network calls fail, retries and timeouts everywhere, no cross-service transactions (see sagas below).
  • Operational overhead — service discovery, per-service CI/CD, distributed tracing, and a platform team to run all of it.
  • Data consistency is now your problem — joins across services don’t exist; you either duplicate data or accept eventual consistency.
Use whenPoor fit when
Multiple teams need independent release cadenceSmall team, one deploy pipeline is still fast enough
Clear domain boundaries (bounded contexts) already existDomain boundaries are still unclear — you’ll draw the wrong lines and pay to redraw them
Different services have very different scaling/reliability needsYou can’t yet operate the platform tax (tracing, gateway, on-call per service)

Avoid

  • Distributed monolith — services that must deploy together and share a database are microservices in name only, with all the network cost and none of the independence.
  • Drawing service boundaries around technical layers (a “database service”, a “validation service”) instead of business capabilities — this forces chatty synchronous calls for every use case.
  • Synchronous call chains four services deep — one slow downstream service now determines the latency (and availability) of every caller above it.

SOA predates microservices and shares the goal of decomposing a system into services, but typically centers on a shared ESB (Enterprise Service Bus) for routing, transformation, and orchestration between services — often to integrate large, heterogeneous, sometimes legacy systems (mainframes, packaged enterprise software) rather than to let small teams ship independently.

d2

SOAMicroservices
IntegrationCentralized ESB does routing/transformation/orchestrationServices talk directly (or via lightweight broker); no central orchestrator
SharingServices often share a common data model and infrastructureEach service owns its own data and schema
GovernanceCentralized, heavyweight, often enterprise-wideDecentralized; each team governs its own service
Typical scaleEnterprise integration across many legacy systemsProduct teams shipping independently at web scale
  • Good at integrating legacy systems — the ESB absorbs protocol and format differences (SOAP, flat files, mainframe calls) so consumers see one contract.
  • Centralized policy enforcement — security, logging, transformation rules live in one place.
  • The ESB becomes a bottleneck — both operationally (single point of failure/scaling) and organizationally (one team owns all integration logic, becoming a queue for every other team’s change).
  • Heavyweight tooling and governance slow down iteration compared to microservices’ decentralized model.
Use whenPoor fit when
Integrating many existing/legacy enterprise systems that can’t changeBuilding a new product where teams should own their own release cycle
Centralized governance is a hard requirement (regulated enterprise IT)You want microservices-style team autonomy — the ESB re-centralizes it

Avoid

  • Adopting a heavyweight ESB for a greenfield product with no legacy integration burden — you inherit SOA’s central bottleneck without needing its legacy-integration benefit.

Event-driven architecture decouples components by having producers publish events (“OrderPlaced”, “PaymentFailed”) to a broker (Kafka, SNS/SQS, EventBridge, RabbitMQ) instead of calling consumers directly. Producers don’t know who — or how many — consumers exist; consumers subscribe to the events they care about.

d2

Two patterns commonly ride on top of an event-driven backbone:

  • Event sourcing — instead of storing current state, you store the sequence of events that produced it (OrderPlaced, OrderShipped, OrderCancelled); current state is derived by replaying events. Gives a full audit trail and the ability to rebuild any past state.
  • CQRS (Command Query Responsibility Segregation) — separate the write model (accepts commands, appends events) from the read model (a denormalized view optimized for queries, kept up to date by projecting events). Often paired with event sourcing, but usable independently.

A one-line description of what this shows: the write path appends an event and returns immediately; the read model catches up asynchronously via a projector.

plantuml

Sagas — when a business process spans multiple services with no shared transaction (e.g. “reserve inventory, charge payment, ship order”), a saga coordinates the steps via events and defines compensating actions (“release inventory”) to undo prior steps if a later one fails, since there is no distributed rollback.

  • Loose coupling — producers and consumers don’t know about each other; add a new consumer without touching the producer.
  • Natural fit for async workflows — notifications, analytics pipelines, cache invalidation, and cross-service workflows that don’t need an immediate response.
  • Event sourcing gives a full audit log for free — useful for compliance, debugging, and time-travel debugging.
  • Eventual consistency — the read model lags the write model by some delay; UX and support teams must account for “why don’t I see my order yet.”
  • Harder to trace — a bug means following an event through a broker and several consumers instead of a single call stack; needs distributed tracing and event schemas from day one.
  • Event sourcing has a real cost — replay logic, schema evolution for old events, and snapshotting for long event streams are non-trivial to build correctly.
Use whenPoor fit when
Multiple services need to react to the same fact (order placed → billing, shipping, analytics)Caller needs an immediate, strongly consistent answer (“is this seat still available right now”)
Cross-service workflows with no shared database (sagas)Team has no experience operating a broker, schema registry, or dead-letter queues
You need a durable audit trail of everything that happened (event sourcing)The domain is simple CRUD with no fan-out to other systems

Avoid

  • Choreography with no map — dozens of services reacting to each other’s events with no central documentation becomes impossible to reason about (“where does this field get set?”). Keep an event catalog.
  • Using events for calls that need a synchronous answer — don’t publish an event and poll for a reply when a direct request/response call is simpler and clearer.
  • Skipping schema versioning on events — a producer changing an event’s shape silently breaks every consumer that assumed the old shape.

6. Serverless / Function-as-a-Service (FaaS)

Section titled “6. Serverless / Function-as-a-Service (FaaS)”

Serverless runs your code as short-lived functions (AWS Lambda, Azure Functions, Cloud Functions) triggered by events — an HTTP request, a queue message, a file upload, a schedule — with the cloud provider managing servers, scaling, and patching entirely. You pay per invocation/duration, not for idle capacity.

d2

  • No server management — no patching, no capacity planning, no idle-fleet cost; scales to zero when unused.
  • Pay per use — cheap for spiky or low-traffic workloads compared to always-on instances.
  • Fast to wire event-driven glue — a function triggered by a queue message or an S3 upload is a few lines of config, not a new deployable service to run.
  • Cold starts — a function with no recent traffic can add hundreds of milliseconds to a few seconds of latency on first invocation.
  • Execution time and resource limits — most FaaS platforms cap run duration and memory; long-running or stateful workloads don’t fit.
  • Vendor lock-in — triggers, IAM, and deployment are tightly coupled to one cloud provider’s specifics.
  • Harder local dev/debugging — simulating the full event/trigger graph locally is harder than running a normal server.
Use whenPoor fit when
Spiky, unpredictable, or low-volume traffic where idle server cost is wastefulSustained high-throughput traffic where always-on instances are cheaper per request
Event-driven glue code (resize an image on upload, process a queue message)Long-running processes (video encoding, long WebSocket sessions, big batch jobs)
Small team wants zero infra to operateLatency-sensitive path that cannot tolerate cold starts

Avoid

  • Building a monolith made of tightly-coupled functions that all deploy together and share a database — this is a distributed monolith with the operational quirks of serverless and none of the independence.
  • Chaining many functions synchronously (function A calls function B calls function C) — cold starts and cost compound; prefer async triggers or move that logic into one function/service.

Briefly, for completeness — these show up less often but are worth recognizing by name:

StyleWhat it isTypical use
Micro-kernel (plug-in) architectureA minimal core system plus independently loadable plug-ins that extend itIDEs, browsers with extensions, CMS platforms (WordPress plugins)
Cell-based / space-based architectureThe system is replicated into independent cells, each self-contained (app + cache + data), sized to a fixed capacity, to bound blast radius and remove shared bottlenecksHigh-scale systems needing predictable scaling and strong failure isolation (e.g. large e-commerce during peak load)
Micro-frontendsThe microservices idea applied to the UI layer — independently built/deployed frontend fragments composed into one pageLarge web apps with many independent frontend teams
Peer-to-peer (P2P)No central server; nodes act as both client and server, communicating directlyFile sharing, some blockchain and messaging systems

StyleDeployment unitCouplingScales byMain risk
MonolithWhole appTight (in-process)Scaling the whole appBlast radius, deploy risk at scale
Layered / hexagonal(Internal structure, not a topology)Tight within the unit, layeredN/A — inherits its host’sLayer leakage over time
MicroservicesPer serviceLoose (network)Per serviceDistributed systems complexity
SOAPer service, via ESBLoose, but centralized via ESBPer service (ESB can bottleneck)Central ESB becomes a bottleneck
Event-drivenPer producer/consumerVery loose (async)Per consumerEventual consistency, hard to trace
ServerlessPer functionVery loose (event-triggered)Automatic, per invocationCold starts, vendor lock-in, limits

Quick decision hints

  • Small team, unproven domain? → Start with a monolith, structured in clear layers (or hexagonal) internally, so a future split is a refactor, not a rewrite.
  • Multiple teams, clear domain boundaries, need independent releases?Microservices.
  • Integrating legacy/enterprise systems you don’t own?SOA with an ESB, or a modern API gateway doing the same job with less ceremony.
  • Many things need to react to the same fact, or you need a cross-service workflow with no shared transaction?Event-driven (with sagas for the workflow, CQRS/event sourcing if you need an audit trail or fast reads).
  • Spiky or low traffic, glue code, no appetite to run servers?Serverless.
  • Real systems are hybrids — a common production shape is: a few microservices, each internally layered, communicating mostly over events, with serverless functions for glue (image resize, scheduled jobs, webhook receivers) and an API gateway in front of all of it.

← API Design Concepts