Introduction: Architectural Patterns #
In software engineering, architectural decisions are the most expensive decisions to change. Frameworks can be swapped in a few sprints. Libraries can be refactored in one iteration. Even programming languages can be migrated — painful, but doable. A wrong architecture, on the other hand, will keep “haunting” the system for years: every new feature feels like swimming against the current, every refactor ripples into unexpected places, and every technical discussion ends with “actually, the root problem from the start was…”. Understanding architectural patterns is not about memorizing diagrams — it is about understanding why a system is shaped a certain way, what is sacrificed, and when a choice starts turning into a burden.
Architectural Pattern vs Design Pattern #
Before diving into the pattern list, it is important to understand where architectural patterns sit in the engineering decision hierarchy.
flowchart TD
AP["Architectural Pattern\\n(system level)"]
DP["Design Pattern\\n(component level)"]
CP["Coding Pattern\\n(function/class level)"]
AP -->|"constrains and shapes"| DP
DP -->|"constrains and shapes"| CP
AP_EX["Microservices, Clean Architecture,\\nEvent-Driven, CQRS"]
DP_EX["Repository, Factory, Strategy,\\nObserver, Decorator"]
CP_EX["Guard clauses, early return,\\nimmutability, tail calls"]
AP --- AP_EX
DP --- DP_EX
CP --- CP_EXThe difference is not just about scale — it is about the kind of decision being made:
| Dimension | Design Pattern | Architectural Pattern |
|---|---|---|
| Level | Classes and functions | Systems and large components |
| Question | How do these objects collaborate? | How is this system divided? |
| Change impact | Localized, a week | Spreads across the codebase, months |
| Who decides | Individual developers | Core team, tech leads, architects |
| Examples | Repository, Strategy | Microservices, Event-Driven |
| When wrong | Bugs or hard-to-test code | A system that is hard to grow for years |
Architectural patterns answer the big questions design patterns cannot:
Questions answered by Architectural Patterns:
□ How is the system divided into large components?
□ How do those components communicate with each other?
□ Where is the boundary between domain, data, and infrastructure?
□ How can the system be deployed, scaled, and changed independently?
□ How can different teams work without waiting on each other?
□ How does the system evolve as teams, traffic, and complexity grow?
Architecture Decision Dimensions #
Every architectural choice is a compromise along several dimensions. There is no “best” architecture — only the one that best fits the context. Understanding these dimensions helps you evaluate options more objectively.
flowchart LR
subgraph DIM["Main Architecture Decision Dimensions"]
D1["Deployment\\nComplexity"]
D2["Operational\\nComplexity"]
D3["Development\\nVelocity"]
D4["Scalability"]
D5["Testability"]
D6["Team\\nAutonomy"]
end
Simple["Monolith\\nLayered"] -.->|"low"| D1
Simple -.->|"low"| D2
Simple -.->|"high early"| D3
Complex["Microservices\\nEvent-Driven"] -.->|"high"| D1
Complex -.->|"high"| D2
Complex -.->|"low early, high when mature"| D3
Complex -.->|"high"| D4
Complex -.->|"high per-service"| D6Deployment Complexity #
How complicated is deploying this system? A monolith deploys once. Microservices can have dozens of separate CI/CD pipelines. Serverless has no servers to manage — but there is vendor lock-in and cold start latency to think about.
Operational Complexity #
How hard is it to run this system in production? One monolith with one log stream and one database is easier to debug than 30 microservices communicating through an event bus. Distributed tracing, service meshes, and circuit breakers are not luxuries — they are necessities in distributed architectures.
Development Velocity #
How fast can the team add new features? In a monolith, one developer can touch the whole stack. In microservices, clear team ownership speeds up per-service development but slows down features requiring cross-service coordination.
Scalability #
How easily can this system handle increased load? A monolith can only scale horizontally as one unit. Microservices allow per-service scaling as needed. CQRS separates read and write scaling independently.
Testability #
How easily can this system’s components be tested in isolation? Clean Architecture and Hexagonal Architecture are explicitly designed to make unit testing easy without a real database or HTTP server. A large, tightly coupled monolith can be very hard to test.
Team Autonomy #
How independently can teams work? This is often more important than purely technical considerations. Conway’s Law states that system structures tend to mirror the communication structures of the organizations that build them.
Architectural Pattern Taxonomy #
There are many ways to categorize architectural patterns. One of the most useful is by distribution level — how distributed the system’s components are.
flowchart TD
subgraph L1["Level 1: Single Deployment Unit"]
M[Monolithic]
MM[Modular Monolith]
end
subgraph L2["Level 2: One Process, Many Layers"]
LA[Layered Architecture]
CA[Clean Architecture]
HA[Hexagonal Architecture]
OA[Onion Architecture]
end
subgraph L3["Level 3: Multiple Services"]
SBA[Service-Based]
MS[Microservices]
end
subgraph L4["Level 4: Event & Async"]
EDA[Event-Driven]
CQRS[CQRS]
SL[Serverless]
end
subgraph L5["Level 5: UI & Presentation"]
MVC[MVC / MVP / MVVM]
end
L1 -->|"organizational scale"| L3
L2 -->|"distribution scale"| L3
L3 -->|"async scale"| L4Monolithic Architecture #
The system is built and deployed as one whole unit. All components live in one codebase and one runtime process. This is not a “wrong” choice — for small teams and evolving systems, the monolith is often the most rational and productive choice. Problems only arise when the monolith grows without structure, turning into a big ball of mud that nobody truly understands in its entirety.
Main strengths: Simple development, single deployment, easy debugging, trivial database transactions.
Weaknesses: All teams deploy together (a bottleneck), one bug can crash the whole system, hard to scale specific parts independently.
Modular Monolith #
An evolution of the monolith: one deployment, but the codebase is divided into modules with firm boundaries and strictly enforced dependency rules. Each module has its own public interface, and other modules must not access a module’s internals directly. When boundaries are maintained with discipline, a modular monolith can become a very effective end-game architecture — no need for microservices if the team and system do not require them.
Main strengths: Simple deployment but organized code, can be extracted into services if needed, safer refactors thanks to firm boundaries.
Weaknesses: Requires high team discipline to maintain boundaries; tooling to validate boundaries is not always available.
Layered Architecture #
The application is divided into horizontal layers — usually presentation, application, domain, and data/infrastructure — with the rule that dependencies only flow downward (upper layers depend on lower layers, not the other way around). This is the architecture most familiar to developers because it is easy to understand and most often taught.
Main strengths: Easy to understand, clear separation of concerns, suitable for junior to mid-level teams.
Weaknesses: Prone to the anemic domain model (the domain only holds data, logic scattered across service layers); the database often becomes the “center of gravity” creating hidden coupling.
Clean Architecture #
Business rules are placed at the center of the system, and dependencies always point inward — outer layers (database, UI, framework) depend on inner layers (domain), never the reverse. This allows domain logic to be tested without a database or framework. Published by Robert C. Martin as a synthesis of various domain-centric approaches.
flowchart TD
subgraph CLEAN["Clean Architecture — Dependency Rule"]
FR["Frameworks & Drivers\\n(UI, DB, Web)"]
IA["Interface Adapters\\n(Controllers, Gateways)"]
UC["Use Cases\\n(Application Business Rules)"]
EN["Entities\\n(Enterprise Business Rules)"]
end
FR -->|"depends on"| IA
IA -->|"depends on"| UC
UC -->|"depends on"| EN
EN -->|"depends on nothing"| ENMain strengths: Domain isolated from infrastructure, very high testability, framework changes do not affect business rules.
Weaknesses: Many layers and interfaces to create, requires significant initial investment, can be over-engineering for small systems.
Hexagonal Architecture (Ports & Adapters) #
The application core interacts with the outside world through ports (interfaces defined by the core) and adapters (concrete implementations filling those ports). Databases, HTTP APIs, message queues, CLIs — all are adapters that can be swapped without touching the core. Introduced by Alistair Cockburn.
flowchart LR
subgraph HEX["Hexagonal Architecture"]
CORE["Application Core\\n(Domain + Use Cases)"]
PP["Primary Port\\n(Driving)"]
SP["Secondary Port\\n(Driven)"]
end
HTTP["HTTP\\nAdapter"] -->|"via port"| PP
CLI["CLI\\nAdapter"] -->|"via port"| PP
PP --> CORE
CORE --> SP
SP -->|"via port"| DB["Database\\nAdapter"]
SP -->|"via port"| MQ["Message Queue\\nAdapter"]Main strengths: Very easy to swap adapters (e.g., switching from MySQL to PostgreSQL, or adding REST alongside gRPC), excellent testability with mock adapters.
Weaknesses: Requires a solid understanding of the port/adapter distinction; can be verbose for simple domains.
Onion Architecture #
The domain model sits at the center, surrounded by layers that become increasingly implementation-specific toward the outside. Similar to Clean Architecture and Hexagonal, but with a different visualization — layers like an onion wrapping the domain core. Grew out of DDD practice.
Main strengths: Pure domain at the center, fits DDD approaches very well, strict dependency rule.
Weaknesses: Similar to Clean Architecture in terms of pros and cons; boundaries between layers can be ambiguous.
Service-Based Architecture #
The application is split into several services larger than microservices — usually 4 to 12 services per system — which often still share one database. This is a pragmatic step when the monolith has grown too large for one team, but the organization is not ready for the full complexity of microservices.
Main strengths: Easier than microservices, can be deployed separately, teams can work more independently.
Weaknesses: A shared database creates hidden coupling; does not provide full deployment isolation.
Microservices Architecture #
The system consists of many small services that can be developed, deployed, and scaled independently. Each service has its own database, communicates via APIs or events, and is usually owned by one small team. Popularized by Netflix, Amazon, and other large-scale companies.
flowchart TD
GW[API Gateway] --> S1[User Service\\nPostgreSQL]
GW --> S2[Order Service\\nMySQL]
GW --> S3[Payment Service\\nPostgreSQL]
S2 -->|"event"| MQ[(Message Broker)]
MQ --> S4[Notification Service\\nRedis]
MQ --> S5[Analytics Service\\nClickHouse]Main strengths: Independent deployment, technology heterogeneity, per-service scaling, team autonomy.
Weaknesses: Distributed system complexity (network failures, latency, eventual consistency), very high operational overhead, hard cross-service debugging.
Microservices is an organizational decision, not just a technical one. If the team is still small and inter-team communication is still easy, microservices will add complexity without proportionate benefit. Most systems do not need microservices — they need a better organized monolith first.
Event-Driven Architecture #
System components communicate through events asynchronously using a message broker or event bus. Producers publish events without knowing who will consume them. Consumers react to relevant events without knowing who produced them. This creates extremely loose coupling.
sequenceDiagram
participant OS as Order Service
participant MB as Message Broker
participant IS as Inventory Service
participant NS as Notification Service
participant AS as Analytics Service
OS->>MB: publish OrderPlaced event
MB-->>IS: OrderPlaced
MB-->>NS: OrderPlaced
MB-->>AS: OrderPlaced
IS->>MB: publish InventoryReserved event
MB-->>OS: InventoryReservedMain strengths: Extreme loose coupling, high throughput, easy to add new consumers without changing the producer.
Weaknesses: Eventual consistency (data is not immediately consistent), debugging event flows is hard, error handling and retry logic are complex.
CQRS (Command Query Responsibility Segregation) #
Separates the model for write operations (Command) and read operations (Query). The write model is optimized for consistency and business validation. The read model is optimized for fast, flexible queries — it can be denormalized views, search indexes, or caches. Promoted by Greg Young and often combined with Event Sourcing.
flowchart LR
C([Client]) -->|"Command"| WM["Write Model\\n(Command Handler)"]
C -->|"Query"| RM["Read Model\\n(Query Handler)"]
WM -->|"write"| WDB[(Write DB\\nnormalized)]
WM -->|"event"| PROJ[Projector]
PROJ -->|"update"| RDB[(Read DB\\ndenormalized)]
RM -->|"read"| RDBMain strengths: Reads and writes can be scaled independently, the read model can be tailored to specific queries, the write model can focus on business invariants.
Weaknesses: Eventual consistency between write and read models, high complexity for simple CRUD systems, more code to manage.
Serverless Architecture #
Application logic runs in small functions triggered by events, without directly managing servers. The cloud provider (AWS Lambda, Google Cloud Functions) handles scaling, availability, and runtime.
Main strengths: Zero operational overhead for infrastructure, pay-per-execution (efficient for uneven traffic), automatic scaling.
Weaknesses: Cold start latency, vendor lock-in, complex state management, limited observability, not suited for long-running processes.
MVC / MVP / MVVM #
Patterns for separating concerns at the UI level between data (Model), view (View), and presentation logic (Controller/Presenter/ViewModel). Originating from MVC in Smalltalk, evolving into MVP for Android and MVVM for modern reactive frameworks.
Main strengths: Clear separation of concerns at the UI layer, easy to test if implemented correctly.
Weaknesses: Business logic often leaks into the Presenter/ViewModel; does not answer questions about deeper layers (data access, domain).
Decision Tree: Choosing the Right Architecture #
There is no perfect formula, but the following questions help narrow the choices:
flowchart TD
Q1{How large are\\nthe team and system?} -->|"< 5 developers\\nor MVP"| MON[Monolith or\\nModular Monolith]
Q1 -->|"5-20 developers\\nor growing system"| Q2
Q2{Is the business\\ndomain complex?} -->|"Yes — many business rules\\nand invariants"| Q3
Q2 -->|"No — CRUD-heavy\\nor data pipelines"| LAY[Layered Architecture]
Q3{How important\\nis testability?} -->|"Very important\\nmany external integrations"| Q4
Q3 -->|"Fairly important"| OA[Onion Architecture]
Q4{Many external\\nintegrations?} -->|"Yes — different DBs,\\nAPIs, message queues"| HEX[Hexagonal Architecture]
Q4 -->|"Not really"| CA[Clean Architecture]
Q1 -->|"> 20 developers\\nor a broad domain"| Q5
Q5{Can the team be split\\nper domain?} -->|"Yes — clear ownership\\nper domain"| Q6
Q5 -->|"No — still one\\nlarge team"| SBA[Service-Based]
Q6{Ready for distributed\\nsystem ops?} -->|"Yes — k8s, tracing,\\nmature monitoring"| MS[Microservices]
Q6 -->|"Not yet"| SBA
MS --> Q7{High async\\nthroughput?}
Q7 -->|"Yes"| EDA[Event-Driven]
Q7 -->|"Read-heavy &\\ncomplex writes"| CQRS[CQRS]
Q7 -->|"Event-based &\\nunstable traffic"| SL[Serverless]Quick Reference Guide #
The following table is a short summary for use during architecture discussions or high-level design reviews:
| Architectural Pattern | Good Fit If | Poor Fit If | Important Notes |
|---|---|---|---|
| Monolithic | Small team, MVP, fast changes | Large team, frequent deployment conflicts | The most rational choice early on |
| Modular Monolith | System growing but want to stay simple | Module boundaries not kept with discipline | Can be an end-game architecture |
| Layered | CRUD-heavy, junior-mid teams | Complex domain logic with rich rules | Easy to understand, prone to anemic domain |
| Clean Architecture | Complex business rules, long system lifetime | Small, stable systems | A long-term investment |
| Hexagonal | Many swappable external integrations | Team unfamiliar with port/adapter concepts | Very strong for testability |
| Onion | Domain-centric and DDD-heavy | Simple domains | Focus on domain purity |
| Service-Based | Monolith too large, gradual transition | Need full deployment isolation | Shared DB is still common |
| Microservices | Large teams, broad domain, clear ownership | Small teams, immature observability | An organizational decision, not just technical |
| Event-Driven | Async processes, high throughput, loose coupling | Debugging must stay simple | Distributed tracing must be mature |
| CQRS | Read-heavy, complex writes, need separate scaling | Simple CRUD | A scalability pattern, not the default |
| Serverless | Unstable traffic, event-based, minimal ops | Latency-sensitive, stateful, long-running | Cold start and vendor lock-in |
| MVC/MVP/MVVM | UI and presentation layer focus | Heavy business logic in presentation | Usually combined with backend patterns |
Architecture Evolution: Not a Once-in-a-Lifetime Decision #
One of the biggest misconceptions about architecture is that you must pick the “right” one from the start. In reality, good architecture is architecture that can evolve as the system and team grow.
flowchart LR
MVP["MVP\\nMonolith"] -->|"team grows\\ndomain becomes clearer"| MM["Modular\\nMonolith"]
MM -->|"boundaries firm up\\nclear team ownership"| SBA["Service-Based\\nArchitecture"]
SBA -->|"high traffic\\nmature ops"| MS["Microservices"]
MS -->|"high async\\nthroughput needed"| EDA["Event-Driven\\n+ CQRS"]Most successful systems start as monoliths and evolve only when there is a real need. Jumping straight to microservices before the domain is well understood — what is often called premature distribution — is a source of far bigger problems than a well-structured monolith.
Signs the architecture needs to evolve:
□ One small change requires coordination with many teams
□ Deploying one part blocks deploying another part
□ The database has become a bottleneck and can no longer be tuned
□ A bug in one module often causes failures in other modules
□ The team struggles to understand the whole system because it is too large
Signs the architecture is too complex for current needs:
□ More time is spent on ops than on development
□ Simple features need coordination across many services
□ Debugging requires tracing dozens of services
□ A small team is overwhelmed by distributed system overhead
How to Read This Section #
Every article in this section follows the same structure:
Core concept → why this pattern exists, what problem it solves
Structure → component diagrams and how they interact
Implementation → concrete code examples in Go
Trade-offs → pros, cons, and when it does not fit
Anti-patterns → common implementation mistakes
Comparison → relative position against similar patterns
Checklist → a review guide before adopting it
This approach emphasizes that every pattern is a tool with trade-offs, not a universal solution. The goal is not for you to implement every pattern, but to make deliberate decisions — knowing exactly what you gain and what you sacrifice.
Summary #
- Architectural patterns work at the system level, not the code level — they answer questions about how large components are divided, how they communicate, and how the system evolves; not how classes or functions are organized.
- There is no “best” architecture — every choice is a compromise between deployment complexity, operational complexity, development velocity, scalability, testability, and team autonomy.
- Start simple, evolve based on real needs — most successful systems start as monoliths and evolve only when there is real pressure; premature distribution is a source of far bigger problems.
- Microservices is an organizational decision — not a solution for slow systems or bad code; if team communication is still easy and the domain is not yet understood, microservices add complexity without benefit.
- Conway’s Law always works — system structures tend to mirror the communication structures of the organization; organization design and architecture design cannot be separated.
- Domain-centric architectures (Clean, Hexagonal, Onion) give the best testability — by keeping dependencies always pointing into the domain, infrastructure changes do not affect business rules.
- Event-Driven and CQRS are advanced patterns requiring prerequisites — distributed tracing, idempotency, and eventual consistency handling must be mature before adopting them.
- The Modular Monolith is often overlooked but very powerful — with firmly kept boundaries, a well-organized monolith can handle many organizations’ needs without distribution complexity.
- Good architecture can evolve — it does not lock you into one shape forever; firm boundaries and clear dependency rules make extracting smaller units possible whenever needed.
- Read every following article as a trade-off study — not as a guide to “when to implement this”, but as a deep understanding of what is gained and sacrificed with each choice.