Skip to content

Architectural Styles & Design Patterns

A style is a blueprint choice made once for a whole system — bungalow, high-rise, or modular units — each suited to different needs, not a personal preference applied inconsistently file by file.

  • What it is: proven, named structural approaches — layered architecture, hexagonal (ports & adapters), microservices vs. monolith, CQRS — for organizing a system.
  • Why it matters: reusing a proven pattern avoids reinventing a poor solution, and gives teams a shared vocabulary for discussing structure.
  • For developers: before introducing a new pattern, check which one the codebase already follows — mixing patterns inconsistently is often worse than either alone.
  • When to use it: when defining the overall structure of a new system, or when an existing system’s structure is fighting the problem it needs to solve.
  • When not to use it: don’t adopt a pattern (e.g. microservices) because it’s popular — adopt it because it solves a problem you actually have (independent scaling, independent deploys, team autonomy).
  • Prerequisites: Service & Component Boundaries.
flowchart TB
    subgraph Hexagonal["Hexagonal (Ports & Adapters)"]
      Core[Business logic]
      Core --- PortA[Port: Payment]
      Core --- PortB[Port: Notification]
      PortA --- AdapterA1[Adapter: Stripe]
      PortA --- AdapterA2[Adapter: Internal gateway]
      PortB --- AdapterB1[Adapter: Email]
    end
  1. Identify the problem the structure needs to solve (independent deploys? swappable external dependencies? a read/write scaling mismatch?).
  2. Match the problem to a known style: layered (simple separation of concerns), hexagonal (swappable external dependencies), microservices (independent deploy/scale per team), CQRS (very different read vs. write demands).
  3. Check what style the codebase already follows before introducing a different one.
  4. Apply the style consistently at the boundary it’s meant for — don’t apply hexagonal ports to only one dependency and not others in the same layer.
  5. Document the chosen style so new contributors recognize the pattern instead of rediscovering it ad hoc.

The payment service’s business logic calls the Stripe SDK directly throughout its code.

  • Input: business logic is hard-wired to the Stripe SDK, making it impossible to add a second provider without rewriting core logic.
  • Process: the team defines a PaymentGateway port (an interface: authorize, capture, refund) that business logic depends on; Stripe becomes one adapter implementing that port, and a new internal gateway becomes a second adapter.
  • Output: business logic no longer knows or cares which provider is plugged in; switching or adding providers means writing a new adapter, not touching core logic.
public interface IPaymentGateway // the port
{
Task<AuthResult> AuthorizeAsync(PaymentRequest request);
}
public sealed class StripePaymentAdapter : IPaymentGateway // adapter 1
{
public Task<AuthResult> AuthorizeAsync(PaymentRequest request) =>
_stripeClient.Charges.CreateAsync(MapToStripeCharge(request));
}
public sealed class InternalGatewayAdapter : IPaymentGateway // adapter 2
{
public Task<AuthResult> AuthorizeAsync(PaymentRequest request) =>
_internalGatewayClient.AuthorizeAsync(MapToInternalRequest(request));
}
// Business logic depends only on the port, injected via DI
public CheckoutService(IPaymentGateway paymentGateway) => _paymentGateway = paymentGateway;
Use case When to use Notes
Swappable external dependency (payment, email, storage) Hexagonal / ports & adapters Core logic depends on an interface, never the vendor SDK directly
Simple CRUD app with few cross-cutting concerns Layered architecture Don’t over-engineer with microservices if one team, one deploy is enough
Independent team scaling/deploys Microservices Only worth the operational cost if team/deploy independence is a real need
Read load vastly exceeds write load, or vice versa CQRS Separates the read and write models so each can be optimized/scaled independently
Event-triggered, bursty, or infrequent workload Serverless / FaaS (e.g. Azure Functions) You pay for execution time, not idle capacity; the platform scales instances for you
  • Layered architecture: organizes code into horizontal layers (presentation, business logic, data access); simple but can become a “big ball of mud” if layers aren’t enforced.
  • Hexagonal / ports & adapters: business logic sits at the center, depending only on interfaces (“ports”); technology-specific code (“adapters”) plugs in from the outside — this is what made the payment gateway example swappable.
  • Microservices vs. monolith: microservices trade simplicity for independent deployability and scaling — appropriate when teams need to move independently, not merely because a system is “big.”
  • CQRS (Command Query Responsibility Segregation): separates the model used for writes from the model used for reads, useful when their performance or scaling needs diverge sharply.
  • Serverless (FaaS) narrows, but doesn’t remove, the build-vs-buy and style decision: Martin Fowler’s Serverless Architectures frames FaaS (e.g. Azure Functions) as trading operational control for automatic scaling and pay-per-execution cost — a legitimate structural choice for the storefront’s stock-updater consumer above, but still one to make deliberately, since cold starts and vendor lock-in are real trade-offs.
Term Meaning
Layered architecture Organizing code into horizontal layers (presentation/business/data)
Hexagonal / ports & adapters Business logic depends on interfaces (“ports”); technology-specific code plugs in as “adapters”
Microservices Independently deployable services, each owning its own data and scaling
CQRS Separating the read model from the write model
Serverless (FaaS) Function-as-a-Service: code that runs on demand, with the platform managing scaling and infrastructure
Symptom Likely cause Fix
Swapping a vendor requires rewriting business logic No port/interface exists between logic and vendor SDK Introduce a port and move vendor-specific code into an adapter
Microservices adopted but every deploy still requires coordinating all of them Services were split without independent bounded contexts Revisit service boundaries before the pattern, not after
Codebase mixes multiple structural styles inconsistently No agreed style was documented Pick one style per boundary and document it
  • Why did making Stripe an adapter (not embedding it in business logic) make the system easier to extend?
  • Name a system you know that adopted microservices — was the independent-deploy/scale need actually present?

Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries, API Design & Contracts, Event-Driven & Asynchronous Communication, and the Glossary.