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.
At a glance
Section titled “At a glance”- 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.
Mental model
Section titled “Mental model”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
Step-by-step walkthrough
Section titled “Step-by-step walkthrough”- Identify the problem the structure needs to solve (independent deploys? swappable external dependencies? a read/write scaling mismatch?).
- 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).
- Check what style the codebase already follows before introducing a different one.
- 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.
- Document the chosen style so new contributors recognize the pattern instead of rediscovering it ad hoc.
Worked example
Section titled “Worked example”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
PaymentGatewayport (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 DIpublic CheckoutService(IPaymentGateway paymentGateway) => _paymentGateway = paymentGateway;Common use cases
Section titled “Common use cases”| 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 |
Advanced details
Section titled “Advanced details”- 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.
Quick reference
Section titled “Quick reference”| 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 |
Troubleshooting
Section titled “Troubleshooting”| 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 |
Check your understanding
Section titled “Check your understanding”- 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?
Related pages
Section titled “Related pages”Part of Introduction to Software & Application Architecture. See also Service & Component Boundaries, API Design & Contracts, Event-Driven & Asynchronous Communication, and the Glossary.