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.

Architectural Patterns in Go

๐Ÿ–ผ 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.
โ”œโ”€ cmd/
โ”‚  โ””โ”€ server/
โ”‚     โ””โ”€ main.go                  # wire HTTP router, DI, config
โ”œโ”€ internal/
โ”‚  โ”œโ”€ app/
โ”‚  โ”‚  โ”œโ”€ http/
โ”‚  โ”‚  โ”‚  โ””โ”€ controllers/          # controllers (handlers) โ€” โ€œCโ€
โ”‚  โ”‚  โ””โ”€ services/                # application/services layer (use cases)
โ”‚  โ”œโ”€ domain/
โ”‚  โ”‚  โ””โ”€ user/                    # domain models & interfaces โ€” โ€œMโ€
โ”‚  โ”‚     โ”œโ”€ model.go
โ”‚  โ”‚     โ”œโ”€ repository.go         # DAO (port)
โ”‚  โ”‚     โ””โ”€ errors.go
โ”‚  โ”œโ”€ infra/
โ”‚  โ”‚  โ”œโ”€ db/                      # db bootstrapping (sqlx/gorm/pgx)
โ”‚  โ”‚  โ””โ”€ repository/              # DAO impls (adapters) โ€” Postgres/MySQL
โ”‚  โ”‚     โ””โ”€ user_pg.go
โ”‚  โ””โ”€ transport/
โ”‚     โ””โ”€ http/
โ”‚        โ”œโ”€ router.go             # gin/chi mux + routes
โ”‚        โ””โ”€ dto/                  # DTOs โ€” request/response
โ”‚           โ””โ”€ user.go
โ”œโ”€ pkg/
โ”‚  โ”œโ”€ logger/
โ”‚  โ””โ”€ validator/
โ””โ”€ go.mod

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package user

import "time"

type ID string

type User struct {
	ID        ID
	Name      string
	Email     string
	Active    bool
	CreatedAt time.Time
}

// Domain invariants/constructors keep the model valid.
func New(name, email string) (User, error) {
	if name == "" {
		return User{}, ErrInvalidName
	}
	if !isValidEmail(email) {
		return User{}, ErrInvalidEmail
	}
	return User{
		ID:     ID(NewID()),
		Name:   name,
		Email:  email,
		Active: true,
		// CreatedAt set in service or repo
	}, nil
}

File: internal/domain/user/repository.go (DAO Port)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package user

import "context"

type Repository interface {
	Create(ctx context.Context, u User) error
	ByID(ctx context.Context, id ID) (User, error)
	ByEmail(ctx context.Context, email string) (User, error)
	List(ctx context.Context, limit, offset int) ([]User, error)
	Update(ctx context.Context, u User) error
	Delete(ctx context.Context, id ID) error
}

๐Ÿงฐ DAO Implementation (Adapter)

File: internal/infra/repository/user_pg.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package repository

import (
	"context"
	"database/sql"
	"errors"
	"time"

	domain "yourapp/internal/domain/user"
)

type UserPG struct {
	db *sql.DB
}

func NewUserPG(db *sql.DB) *UserPG { return &UserPG{db: db} }

func (r *UserPG) Create(ctx context.Context, u domain.User) error {
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO users (id, name, email, active, created_at)
         VALUES ($1,$2,$3,$4,$5)`,
		u.ID, u.Name, u.Email, u.Active, time.Now().UTC(),
	)
	return err
}

func (r *UserPG) ByID(ctx context.Context, id domain.ID) (domain.User, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT id, name, email, active, created_at FROM users WHERE id=$1`, id)
	var u domain.User
	if err := row.Scan(&u.ID, &u.Name, &u.Email, &u.Active, &u.CreatedAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return domain.User{}, domain.ErrNotFound
		}
		return domain.User{}, err
	}
	return u, nil
}

// ... ByEmail, List, Update, Delete similarly

๐Ÿง  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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package services

import (
	"context"
	"time"

	domain "yourapp/internal/domain/user"
)

type UserService struct {
	repo domain.Repository
}

func NewUserService(repo domain.Repository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) Register(ctx context.Context, name, email string) (domain.User, error) {
	u, err := domain.New(name, email)
	if err != nil {
		return domain.User{}, err
	}
	// set creation time here if not in repo
	u.CreatedAt = time.Now().UTC()
	if err := s.repo.Create(ctx, u); err != nil {
		return domain.User{}, err
	}
	return u, nil
}

func (s *UserService) Get(ctx context.Context, id domain.ID) (domain.User, error) {
	return s.repo.ByID(ctx, id)
}

๐Ÿ“ฆ DTOs (Requests/Responses)

  • Keep validation on DTOs (with validator).

  • Mapping functions isolate domain โ†”๏ธ transport conversion.

File: internal/transport/http/dto/user.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package dto

import (
	domain "yourapp/internal/domain/user"
)

type CreateUserRequest struct {
	Name  string `json:"name" validate:"required,min=2"`
	Email string `json:"email" validate:"required,email"`
}

type UserResponse struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Email  string `json:"email"`
	Active bool   `json:"active"`
}

func ToUserResponse(u domain.User) UserResponse {
	return UserResponse{
		ID:     string(u.ID),
		Name:   u.Name,
		Email:  u.Email,
		Active: u.Active,
	}
}

๐ŸŽฎ Controller (C) โ€” HTTP Handlers

File: internal/app/http/controllers/user_controller.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package controllers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"yourapp/internal/app/services"
	"yourapp/internal/transport/http/dto"
	domain "yourapp/internal/domain/user"
)

type UserController struct {
	svc *services.UserService
}

func NewUserController(svc *services.UserService) *UserController {
	return &UserController{svc: svc}
}

func (uc *UserController) Register(c *gin.Context) {
	var req dto.CreateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_payload"})
		return
	}
	// optional: validate req with pkg/validator

	user, err := uc.svc.Register(c.Request.Context(), req.Name, req.Email)
	if err != nil {
		switch err {
		case domain.ErrInvalidEmail, domain.ErrInvalidName:
			c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
		default:
			c.JSON(http.StatusInternalServerError, gin.H{"error": "internal_error"})
		}
		return
	}
	c.JSON(http.StatusCreated, dto.ToUserResponse(user))
}

func (uc *UserController) Get(c *gin.Context) {
	id := domain.ID(c.Param("id"))
	user, err := uc.svc.Get(c.Request.Context(), id)
	if err != nil {
		if err == domain.ErrNotFound {
			c.JSON(http.StatusNotFound, gin.H{"error": "not_found"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal_error"})
		return
	}
	c.JSON(http.StatusOK, dto.ToUserResponse(user))
}

File: internal/transport/http/router.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package http

import (
	"github.com/gin-gonic/gin"
	"yourapp/internal/app/http/controllers"
)

func NewRouter(userCtrl *controllers.UserController) *gin.Engine {
	r := gin.New()
	r.Use(gin.Recovery())

	v1 := r.Group("/v1")
	{
		v1.POST("/users", userCtrl.Register)
		v1.GET("/users/:id", userCtrl.Get)
	}
	return r
}

๐Ÿš€ Wiring (main)

File: cmd/server/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"database/sql"
	"log"
	"net/http"

	_ "github.com/lib/pq"

	"yourapp/internal/app/http/controllers"
	"yourapp/internal/app/services"
	"yourapp/internal/infra/repository"
	transport "yourapp/internal/transport/http"
)

func main() {
	db, err := sql.Open("postgres", "postgres://user:pass@localhost:5432/app?sslmode=disable")
	if err != nil { log.Fatal(err) }

	userRepo := repository.NewUserPG(db)          // DAO impl
	userSvc  := services.NewUserService(userRepo) // Service
	userCtrl := controllers.NewUserController(userSvc)

	router := transport.NewRouter(userCtrl)
	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", router))
}

โœ… 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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.
โ”œโ”€ cmd/api/                         # app entrypoint(s)
โ”‚  โ””โ”€ main.go
โ”œโ”€ internal/
โ”‚  โ”œโ”€ domain/                       # PURE core (no imports of gin/sql/http)
โ”‚  โ”‚  โ””โ”€ user/
โ”‚  โ”‚     โ”œโ”€ entity.go               # entities/value objects
โ”‚  โ”‚     โ”œโ”€ service.go              # domain services (pure)
โ”‚  โ”‚     โ”œโ”€ ports.go                # ports (interfaces) e.g., UserRepo, Mailer
โ”‚  โ”‚     โ””โ”€ errors.go
โ”‚  โ”œโ”€ app/                          # use-cases/application services
โ”‚  โ”‚  โ””โ”€ user/
โ”‚  โ”‚     โ””โ”€ usecase.go              # RegisterUser, ActivateUser, etc.
โ”‚  โ”œโ”€ adapters/
โ”‚  โ”‚  โ”œโ”€ in/                        # driving adapters
โ”‚  โ”‚  โ”‚  โ””โ”€ http/                   # HTTP handlers (gin/chi)
โ”‚  โ”‚  โ”‚     โ”œโ”€ router.go
โ”‚  โ”‚  โ”‚     โ””โ”€ user_controller.go
โ”‚  โ”‚  โ””โ”€ out/                       # driven adapters
โ”‚  โ”‚     โ”œโ”€ postgres/
โ”‚  โ”‚     โ”‚  โ””โ”€ user_repo.go         # implements domain.UserRepository
โ”‚  โ”‚     โ””โ”€ mail/
โ”‚  โ”‚        โ””โ”€ smtp_mailer.go       # implements domain.Mailer
โ”‚  โ””โ”€ platform/                     # cross-cutting infra (db, config, log)
โ”‚     โ”œโ”€ db.go
โ”‚     โ”œโ”€ config.go
โ”‚     โ””โ”€ logger.go
โ””โ”€ go.mod

Minimal Go example

File: internal/domain/user/ports.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package user

import "context"

type Repository interface {
	Save(ctx context.Context, u User) error
	ByID(ctx context.Context, id ID) (User, error)
}

type Mailer interface {
	SendWelcome(ctx context.Context, email string) error
}

File: internal/app/user/usecase.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package userapp

import (
	"context"

	domain "yourapp/internal/domain/user"
)

type RegisterUser struct {
	Repo   domain.Repository
	Mailer domain.Mailer
}

func (uc RegisterUser) Do(ctx context.Context, name, email string) (domain.User, error) {
	u, err := domain.New(name, email)
	if err != nil {
		return domain.User{}, err
	}
	if err := uc.Repo.Save(ctx, u); err != nil {
		return domain.User{}, err
	}
	_ = uc.Mailer.SendWelcome(ctx, u.Email) // best-effort, log on failure
	return u, nil
}

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:

1
2
3
4
5
6
7
8
9
// Command Handler
func CreateUser(repo UserRepository, user User) error {
    return repo.Save(user)
}

// Query Handler
func GetUser(repo UserRepository, id int) (User, error) {
    return repo.Find(id)
}

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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.
โ”œโ”€ services/
โ”‚  โ”œโ”€ usersvc/
โ”‚  โ”‚  โ”œโ”€ cmd/usersvc/main.go
โ”‚  โ”‚  โ”œโ”€ internal/...
โ”‚  โ”‚  โ””โ”€ api/openapi.yaml
โ”‚  โ”œโ”€ ordersvc/
โ”‚  โ”‚  โ”œโ”€ cmd/ordersvc/main.go
โ”‚  โ”‚  โ”œโ”€ internal/...
โ”‚  โ”‚  โ””โ”€ api/openapi.yaml
โ”‚  โ””โ”€ paymentsvc/
โ”‚     โ”œโ”€ cmd/paymentsvc/main.go
โ”‚     โ”œโ”€ internal/...
โ”‚     โ””โ”€ api/openapi.yaml
โ”œโ”€ pkg/                         # shared libs (careful: avoid domain leakage)
โ”‚  โ”œโ”€ logger/
โ”‚  โ”œโ”€ tracing/
โ”‚  โ””โ”€ httpx/
โ”œโ”€ deploy/
โ”‚  โ”œโ”€ k8s/                      # Helm/Manifests
โ”‚  โ””โ”€ infra/                    # Terraform (DBs, queues, buckets)
โ””โ”€ Makefile / Taskfile.yaml

Minimal service skeleton (Gin)

File: services/usersvc/cmd/usersvc/main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.New()
	r.Use(gin.Recovery())

	r.GET("/v1/users/:id", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"id": c.Param("id"), "name": "Alice"})
	})

	log.Println("usersvc listening on :8081")
	log.Fatal(http.ListenAndServe(":8081", r))
}

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).


๐Ÿงฑ Clean Architecture

Clean Architecture, introduced by Robert C. Martin (Uncle Bob), is an evolution of Hexagonal and Onion architectures.
Itโ€™s designed to keep your core business logic independent from frameworks, databases, and user interfaces.

๐ŸŽฏ Goal

Source code dependencies always point inward, toward the business logic.

That means:

  • The domain and use cases know nothing about HTTP, databases, or frameworks.
  • Outer layers (HTTP, DB, UI) depend on inner layers, never the other way around.

๐Ÿ› Typical Layers

graph TD
    UI["Presentation<br>(HTTP/gRPC/UI)"]
    UC["Application<br>(Use Cases)"]
    DOMAIN["Domain<br>(Entities, Rules)"]
    INFRA["Infrastructure<br>(DB, APIs, Frameworks)"]

    UI --> UC
    UC --> DOMAIN
    DOMAIN --> INFRA

    style DOMAIN fill:#ffca28,stroke:#f57f17,color:#000
    style UC fill:#66bb6a,stroke:#2e7d32,color:#fff
    style UI fill:#29b6f6,stroke:#0277bd,color:#fff
    style INFRA fill:#8d6e63,stroke:#4e342e,color:#fff
  1. Domain Layer (Entities)

Pure business rules โ€” no dependencies, no frameworks, no I/O.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Order struct {
    ID     string
    Amount float64
}

func (o *Order) Validate() error {
    if o.Amount <= 0 {
        return errors.New("invalid amount")
    }
    return nil
}
  1. Application Layer (Use Cases)

Defines what the system does โ€” coordinates entities and repositories.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type OrderRepository interface {
    Save(order *Order) error
}

type CreateOrder struct {
    Repo OrderRepository
}

func (uc *CreateOrder) Execute(o *Order) error {
    if err := o.Validate(); err != nil {
        return err
    }
    return uc.Repo.Save(o)
}
  1. . Interface Layer (Controllers / Delivery)

Adapts input (HTTP, CLI, gRPC) to use cases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (h *OrderHandler) Create(c *gin.Context) {
    var order Order
    _ = json.NewDecoder(c.Request.Body).Decode(&order)

    if err := h.uc.Execute(&order); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.Status(http.StatusCreated)
}
  1. Infrastructure Layer (Adapters)

Implements the interfaces the core defines โ€” DBs, external APIs, etc.

1
2
3
4
5
6
7
8
type PostgresOrderRepo struct {
    db *sql.DB
}

func (r *PostgresOrderRepo) Save(o *Order) error {
    _, err := r.db.Exec("INSERT INTO orders (id, amount) VALUES ($1,$2)", o.ID, o.Amount)
    return err
}

๐Ÿงฉ Example Go Layout

1
2
3
4
5
6
7
/internal
  /domain       # entities, pure logic
  /application  # use cases, ports
  /interface    # controllers, transport
  /infrastructure # db, external adapters
/cmd
  main.go        # wiring, DI

Main.go wiring:

1
2
3
repo := infrastructure.NewPostgresOrderRepo(db)
uc := application.NewCreateOrder(repo)
handler := interfaces.NewOrderHandler(uc)

โš–๏ธ Clean vs Hexagonal

ConceptHexagonalClean Architecture
FocusPorts & AdaptersDependency direction
ModelTwo-sided (in/out adapters)Concentric rings (domain center)
OriginAlistair CockburnRobert C. Martin
Key RuleDomain defines portsInner layers never depend on outer ones
Example in GoDefine UserRepo in domain, implement in infraSame, but structured explicitly by layer

In practice, Hexagonal = implementation style,

Clean Architecture = guiding principle.

โœ… When to Use

  • You want long-lived codebases that outlive frameworks.

  • You plan to swap out transport (HTTP โ†’ gRPC) or storage (Postgres โ†’ DynamoDB).

  • You want testable, independent domain logic.

โš ๏ธ Pitfalls

  • Can be overkill for small apps.

  • Too many layers = friction if not disciplined.

  • Needs thoughtful dependency injection.

๐Ÿ’ฌ Takeaway

โ€œFrameworks are tools, not architectures.โ€ โ€“ Robert C. Martin

Clean Architecture keeps your business rules pure and your frameworks replaceable.

Itโ€™s not anti-framework โ€” it just ensures you own your core, not the other way around.


๐Ÿ”š 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.