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_EX

The difference is not just about scale — it is about the kind of decision being made:

DimensionDesign PatternArchitectural Pattern
LevelClasses and functionsSystems and large components
QuestionHow do these objects collaborate?How is this system divided?
Change impactLocalized, a weekSpreads across the codebase, months
Who decidesIndividual developersCore team, tech leads, architects
ExamplesRepository, StrategyMicroservices, Event-Driven
When wrongBugs or hard-to-test codeA 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"| D6

Deployment 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"| L4

Monolithic 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"| EN

Main 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: InventoryReserved

Main 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"| RDB

Main 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 PatternGood Fit IfPoor Fit IfImportant Notes
MonolithicSmall team, MVP, fast changesLarge team, frequent deployment conflictsThe most rational choice early on
Modular MonolithSystem growing but want to stay simpleModule boundaries not kept with disciplineCan be an end-game architecture
LayeredCRUD-heavy, junior-mid teamsComplex domain logic with rich rulesEasy to understand, prone to anemic domain
Clean ArchitectureComplex business rules, long system lifetimeSmall, stable systemsA long-term investment
HexagonalMany swappable external integrationsTeam unfamiliar with port/adapter conceptsVery strong for testability
OnionDomain-centric and DDD-heavySimple domainsFocus on domain purity
Service-BasedMonolith too large, gradual transitionNeed full deployment isolationShared DB is still common
MicroservicesLarge teams, broad domain, clear ownershipSmall teams, immature observabilityAn organizational decision, not just technical
Event-DrivenAsync processes, high throughput, loose couplingDebugging must stay simpleDistributed tracing must be mature
CQRSRead-heavy, complex writes, need separate scalingSimple CRUDA scalability pattern, not the default
ServerlessUnstable traffic, event-based, minimal opsLatency-sensitive, stateful, long-runningCold start and vendor lock-in
MVC/MVP/MVVMUI and presentation layer focusHeavy business logic in presentationUsually 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.

← Previous: Guarded Suspension   Next: Clean Architecture →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact