Design patterns (like Singleton, Factory, Strategy) are reusable solutions to small-scale design problems.
Architectural patterns, on the other hand, define the big picture of how systems are structured, how components interact, and how responsibilities are separated.
In this article, weโll explore four influential architectural patterns โ MVC, Hexagonal, CQRS, and Microservices โ and see how they apply to Go development.

๐ผ MVC (ModelโViewโController)
The MVC pattern splits an application into three roles:
- Model โ data and business logic
- View โ presentation layer (UI, HTML templates, JSON responses)
- Controller โ handles input and orchestrates between Model and View
Goal: keep responsibilities crisp.
Model (Domain): business entities + rules (no framework details).
View: JSON/HTML returned to the client.
Controller: HTTP entrypoint; validates input, calls services, shapes output.
DTO (Data Transfer Object): API-facing structs (request/response). Shields the domain from external shape changes.
DAO/Repository: persistence port; hides database from the domain. (DAO is essentially a repository here.)
Why DTO + DAO with MVC?
DTOs prevent leaking internal domain fields (e.g., hashed passwords, internal IDs) and stabilize your public API.
DAO/Repository makes business logic testable (swap Postgres for in-memory in tests) and keeps SQL out of controllers.
Suggested backend layout (Go)
Domain has zero dependency on infra (DB/HTTP).
Services orchestrate use cases.
Controllers adapt HTTP โ๏ธ services and map DTO โ๏ธ Model.
|
|
MVC Diagram.
flowchart LR
subgraph Client["Client (Frontend/UI)"]
A["HTTP Request\nJSON Payload"]
end
subgraph Transport["Transport Layer"]
B["Controller\nGin/Chi Handler"]
C["DTO\nRequest/Response"]
end
subgraph App["Application Layer"]
D["Service\nBusiness Use Case"]
end
subgraph Domain["Domain Layer"]
E["Domain Model\nEntities & Rules"]
F["Repository Interface\n(DAO Port)"]
end
subgraph Infra["Infrastructure Layer"]
G["Repository Impl\nPostgres Adapter"]
H[("Database\nPostgres")]
end
A -->|HTTP JSON| B
B -->|Map to DTO| C
C -->|Pass Valid Data| D
D -->|Use Entities| E
D -->|Call Port| F
F -->|Implemented by| G
G -->|SQL Queries| H
H -->|Result Rows| G
G -->|Return Entities| F
F -->|Back to| D
D -->|Domain โ DTO| C
C -->|JSON Response| B
B -->|HTTP Response| A
๐งฉ Domain Model (M)
File: internal/domain/user/model.go
|
|
File: internal/domain/user/repository.go (DAO Port)
|
|
๐งฐ DAO Implementation (Adapter)
File: internal/infra/repository/user_pg.go
|
|
๐ง Application Service (Use Case Layer)
Services speak domain and depend on ports (interfaces).
Theyโre trivial to unit-test with a fake repository.
File: internal/app/services/user_service.go
|
|
๐ฆ DTOs (Requests/Responses)
Keep validation on DTOs (with validator).
Mapping functions isolate domain โ๏ธ transport conversion.
File: internal/transport/http/dto/user.go
|
|
๐ฎ Controller (C) โ HTTP Handlers
File: internal/app/http/controllers/user_controller.go
|
|
File: internal/transport/http/router.go
|
|
๐ Wiring (main)
File: cmd/server/main.go
|
|
โ Checklist & Tips
Controller: tiny; only HTTP + mapping + error codes.
DTO: versioned (e.g., /v1), validated, stable to external changes.
Service: business orchestration; no HTTP/SQL.
Domain: invariants, pure logic; no frameworks.
DAO/Repository: concrete DB code; easily mocked.
Testing: unit test services with in-memory repo; integration test DAO with a test DB.
Errors: map domain errors to HTTP status codes in controllers.
Versioning: keep DTOs under transport/http/dto/v1 if you plan multiple API versions.
โ When to use:
Web applications with clear input/output flows
Great for monolithic Go services
โ ๏ธ Pitfall: Controllers can easily become โfatโ if not managed well.
๐ก Hexagonal Architecture (Ports & Adapters)
Idea: keep your domain core pure and push frameworks, DBs, and transports to the edges.
Domain (core): entities, value objects, domain services, errors.
Ports: interfaces the core depends on (e.g., UserRepository, Mailer).
Adapters: implementations for ports (Postgres, Redis, SMTP, HTTP clients).
Drivers: incoming adapters (HTTP/gRPC/CLI/Jobs) that call the core.
Suggested layout (Go)
|
|
Minimal Go example
File: internal/domain/user/ports.go
|
|
File: internal/app/user/usecase.go
|
|
Hexagonal Diagram
flowchart LR
subgraph Drivers["Drivers (Incoming Adapters)"]
A["HTTP\n(gin/chi)"]
B["CLI\nCron/Jobs"]
C["gRPC\nGateway"]
end
subgraph Core["Core (Domain + Application)"]
D["Application Services\nUse Cases"]
E["Domain Entities\nValue Objects\nDomain Services"]
F["Ports\n(Repo, Mailer, Cache)"]
end
subgraph Adapters["Driven Adapters (Infra)"]
G["Postgres Repo\nimplements Repo"]
H["SMTP Mailer\nimplements Mailer"]
I["Redis Cache\nimplements Cache"]
end
A --> D
B --> D
C --> D
D --> E
D --> F
F --> G
F --> H
F --> I
โ Checklist & Tips
Keep internal/domain import-clean (no framework/DB imports).
Define ports in the domain; implement them in adapters.
Tests: unit-test use-cases with fakes for ports; integration-test adapters.
โ When to use:
Systems with high read/write load
Event-sourced systems (CQRS often pairs with Event Sourcing)
โ ๏ธ Pitfall: Adds complexity โ not always worth it for simple apps.
๐ CQRS (Command Query Responsibility Segregation)
CQRS separates read and write responsibilities into different models:
Commands โ update state (writes)
Queries โ read state (reads)
This avoids having one bloated model handling both responsibilities.
๐ Go Example:
|
|
CQRS Diagram
flowchart LR
subgraph API["API Layer"]
A["HTTP Endpoints\n/commands\n/queries"]
end
subgraph WriteSide["Write Side"]
B["Command Handler\n(Validate + Execute)"]
C["Write Model\n(Domain + Repo)"]
D[("Write DB")]
end
subgraph ReadSide["Read Side"]
E["Projector\n(Update Projections)"]
F["Read Model\n(DTO Repository)"]
G[("Read DB")]
H["Query Handler\n(Reads Only)"]
end
subgraph Bus["Event Bus (Optional)"]
X["Domain Events"]
end
%% Command path
A -->|POST /commands| B
B --> C
C --> D
C -->|Emit Events| X
%% Projection path
X --> E
E --> F
F --> G
%% Query path
A -->|GET /queries| H
H --> F
%% Notes
classDef dim fill:#f7f7f7,stroke:#bbb,color:#333
class API,WriteSide,ReadSide,Bus dim
โ When to use:
Systems with high read/write load
Event-sourced systems (CQRS often pairs with Event Sourcing)
โ ๏ธ Pitfall: Adds complexity โ not always worth it for simple apps.
โ๏ธ Microservices
A Microservices architecture structures applications as a collection of small, independent services:
Each service owns its data and logic
Services communicate via APIs (HTTP, gRPC, messaging)
Each can be deployed and scaled independently
Idea: split a system into small, independently deployable services that each own their data and domain.
Services communicate via synchronous (HTTP/gRPC) or asynchronous (Kafka/NATS) channels.
Each service has its own database (no shared schema).
Requires solid platform engineering: CI/CD, observability, API governance, SLOs.
Suggested repo layout (mono-repo)
|
|
Minimal service skeleton (Gin)
File: services/usersvc/cmd/usersvc/main.go
|
|
Microservice Diagram
flowchart LR
subgraph Clients["Clients"]
A["Web\nMobile"]
end
subgraph Edge["API Gateway\nIngress"]
B["Routing\nAuth\nRate Limit"]
end
subgraph Services["Microservices"]
U["User Service\nDB owned by service"]
O["Order Service\nDB owned by service"]
P["Payment Service\nDB owned by service"]
end
subgraph Async["Async Messaging"]
K["Kafka/NATS\nTopics"]
end
A --> B
B --> U
B --> O
B --> P
O <-->|Events| K
P <-->|Events| K
U <-->|Events| K
โ Checklist & Tips
Start with modular monolith โ extract services when boundaries stabilize.
Each service: own DB schema, own CI, own versioning.
Invest early in observability (traces, logs, metrics) and API contracts.
โ When to use:
Large, complex systems needing scalability
Teams working on independent modules
โ ๏ธ Pitfall: Operational overhead (DevOps, CI/CD, observability, networking).
๐ Wrap-up pointers
Hexagonal: best baseline for testability and longevity; add adapters as you go.
CQRS: apply where read/write divergence brings value; donโt over-split prematurely.
Microservices: only when team size, domain boundaries, and scaling needs justify the operational cost.
๐ง Summary
MVC โ Clear separation of concerns in monolithic apps
Hexagonal โ Isolate core logic, improve testability
CQRS โ Split reads and writes for clarity and scalability
Microservices โ Independent, scalable services for large systems
๐ Think of it this way:
Design patterns = small tools (Singleton, Observer, Strategy)
Architectural patterns = the blueprint of the entire building
Both are essential, but at different levels of abstraction.
๐ Follow me on norbix.dev for more insights on Go, Python, AI, system design, and engineering wisdom.