Clean Architecture #

Every long-lived system faces the same questions: how do you change the database without touching business logic? How do you test use cases without running a server? How do you add a new channel — REST, gRPC, CLI — without duplicating core logic? These questions cannot be answered by a framework or database choice — they are answered by architecture. Clean Architecture, popularized by Robert C. Martin, answers all of them with one fundamental rule: dependencies must always flow inward, toward the domain. Frameworks, databases, and UIs are implementation details that can be swapped. Business rules are the core that must be protected from all those changes.

The One Rule That Defines Everything #

Clean Architecture can be simplified to a single rule called the Dependency Rule: code in an inner layer must not know anything about an outer layer.

flowchart TD
    subgraph CLEAN["Clean Architecture — Dependency Rule"]
        FW["Frameworks & Drivers\\nWeb, DB, External APIs, UI, CLI"]
        IA["Interface Adapters\\nControllers, Presenters, Gateways, Repositories"]
        UC["Use Cases\\nApplication Business Rules"]
        EN["Entities\\nEnterprise Business Rules"]
    end

    FW -->|"depends on"| IA
    IA -->|"depends on"| UC
    UC -->|"depends on"| EN
    EN -.->|"depends on\\nnobody"| EN

    style EN fill:#2d6a4f,color:#fff
    style UC fill:#40916c,color:#fff
    style IA fill:#74c69d,color:#1b4332
    style FW fill:#b7e4c7,color:#1b4332

Arrows always point inward. Entities know nothing about Use Cases. Use Cases know nothing about Controllers. Controllers know nothing about the HTTP framework. This is not just a convention — it is a structural guarantee that business rules are protected from technology changes.

If you need to change the database from PostgreSQL to MongoDB, only the outermost layer changes. If you need to add GraphQL alongside REST, only a new adapter is added. Entities and Use Cases need not be touched at all.


The Four Layers and Their Responsibilities #

Entities — Enterprise Business Rules #

Entities are objects that encapsulate the most fundamental business rules — rules that apply across the whole enterprise, not just one application. They know nothing about databases, HTTP, or any framework.

// Entity: Order — business rules that apply in every context
package domain

import (
	"errors"
	"time"
)

type OrderStatus string

const (
	StatusPending   OrderStatus = "pending"
	StatusConfirmed OrderStatus = "confirmed"
	StatusShipped   OrderStatus = "shipped"
	StatusCancelled OrderStatus = "cancelled"
)

// Order is an entity encapsulating order business rules
// No imports from databases, HTTP, or any framework
type Order struct {
	id         string
	customerID string
	items      []OrderItem
	status     OrderStatus
	total      Money
	createdAt  time.Time
}

type OrderItem struct {
	productID string
	quantity  int
	price     Money
}

// NewOrder is a factory function enforcing business rules
func NewOrder(customerID string, items []OrderItem) (*Order, error) {
	if customerID == "" {
		return nil, errors.New("customer ID must not be empty")
	}
	if len(items) == 0 {
		return nil, errors.New("order must have at least one item")
	}

	total := Money{Amount: 0, Currency: "IDR"}
	for _, item := range items {
		if item.quantity <= 0 {
			return nil, errors.New("quantity must be greater than 0")
		}
		total.Amount += item.price.Amount * int64(item.quantity)
	}

	return &Order{
		id:         generateID(),
		customerID: customerID,
		items:      items,
		status:     StatusPending,
		total:      total,
		createdAt:  time.Now(),
	}, nil
}

// Confirm changes the order status — enforcing a business invariant
func (o *Order) Confirm() error {
	if o.status != StatusPending {
		return errors.New("only pending orders can be confirmed")
	}
	o.status = StatusConfirmed
	return nil
}

// Cancel cancels the order with strict business rules
func (o *Order) Cancel() error {
	if o.status == StatusShipped || o.status == StatusCancelled {
		return errors.New("orders already shipped or cancelled cannot be cancelled")
	}
	o.status = StatusCancelled
	return nil
}

// Getters — the entity controls access to its internal state
func (o *Order) ID() string          { return o.id }
func (o *Order) CustomerID() string  { return o.customerID }
func (o *Order) Status() OrderStatus { return o.status }
func (o *Order) Total() Money        { return o.total }
func (o *Order) Items() []OrderItem  { return append([]OrderItem{}, o.items...) }

Use Cases — Application Business Rules #

A Use Case orchestrates the specific workflow of one application. It knows about Entities and calls repositories/services through interfaces it defines itself.

// Use Case: PlaceOrder — the specific business flow of this application
package usecase

import (
	"context"
	"time"

	"myapp/domain"
)

// OrderRepository is an interface defined by the Use Case
// Its implementation lives in the outer layer (Infrastructure)
// ✓ CORRECT: the Use Case defines its needs, not infrastructure
type OrderRepository interface {
	Save(ctx context.Context, order *domain.Order) error
	FindByID(ctx context.Context, id string) (*domain.Order, error)
	FindByCustomer(ctx context.Context, customerID string) ([]*domain.Order, error)
}

// InventoryService is an interface for checking stock
type InventoryService interface {
	CheckAndReserve(ctx context.Context, items []domain.OrderItem) error
}

// NotificationService is an interface for sending notifications
type NotificationService interface {
	NotifyOrderPlaced(ctx context.Context, order *domain.Order) error
}

// PlaceOrderInput is the input DTO for this use case
type PlaceOrderInput struct {
	CustomerID string
	Items      []PlaceOrderItem
}

type PlaceOrderItem struct {
	ProductID string
	Quantity  int
	Price     domain.Money
}

// PlaceOrderOutput is the output DTO
type PlaceOrderOutput struct {
	OrderID   string
	Total     domain.Money
	Status    string
	CreatedAt time.Time
}

// PlaceOrderUseCase implements the "customer places an order" logic
type PlaceOrderUseCase struct {
	orderRepo    OrderRepository
	inventory    InventoryService
	notification NotificationService
}

func NewPlaceOrderUseCase(
	orderRepo OrderRepository,
	inventory InventoryService,
	notification NotificationService,
) *PlaceOrderUseCase {
	return &PlaceOrderUseCase{
		orderRepo:    orderRepo,
		inventory:    inventory,
		notification: notification,
	}
}

// Execute runs the use case — it knows nothing about HTTP, databases, or frameworks
func (uc *PlaceOrderUseCase) Execute(ctx context.Context, input PlaceOrderInput) (*PlaceOrderOutput, error) {
	// Convert input to domain objects
	items := make([]domain.OrderItem, len(input.Items))
	for i, item := range input.Items {
		items[i] = domain.OrderItem{
			ProductID: item.ProductID,
			Quantity:  item.Quantity,
			Price:     item.Price,
		}
	}

	// Create the Order entity — business validation lives inside the entity
	order, err := domain.NewOrder(input.CustomerID, items)
	if err != nil {
		return nil, err
	}

	// Check and reserve stock through an interface
	if err := uc.inventory.CheckAndReserve(ctx, items); err != nil {
		return nil, err
	}

	// Save the order through the repository interface
	if err := uc.orderRepo.Save(ctx, order); err != nil {
		return nil, err
	}

	// Send a notification — if it fails, just log it, do not fail the order
	_ = uc.notification.NotifyOrderPlaced(ctx, order)

	return &PlaceOrderOutput{
		OrderID:   order.ID(),
		Total:     order.Total(),
		Status:    string(order.Status()),
		CreatedAt: time.Now(),
	}, nil
}

Interface Adapters — Translators between Use Cases and the Outside World #

Interface Adapters translate data between the format Use Cases understand (domain objects, DTOs) and the format the outside world uses (JSON, SQL rows, gRPC messages).

// Controller: an HTTP handler translating HTTP requests to Use Case calls
package http

import (
	"encoding/json"
	"net/http"

	"myapp/domain"
	"myapp/usecase"
)

// PlaceOrderRequest is the JSON structure received from the client
// ✓ Transport models are separated from domain entities
type PlaceOrderRequest struct {
	CustomerID string              `json:"customer_id"`
	Items      []PlaceOrderItemReq `json:"items"`
}

type PlaceOrderItemReq struct {
	ProductID string `json:"product_id"`
	Quantity  int    `json:"quantity"`
	Price     int64  `json:"price"`
}

// PlaceOrderResponse is the JSON structure sent to the client
type PlaceOrderResponse struct {
	OrderID string `json:"order_id"`
	Total   int64  `json:"total"`
	Status  string `json:"status"`
}

// OrderController is responsible only for:
// 1. Parsing and validating the request
// 2. Mapping to Use Case input
// 3. Calling the Use Case
// 4. Mapping output to the response
// There is NO business logic here
type OrderController struct {
	placeOrderUC *usecase.PlaceOrderUseCase
}

func NewOrderController(placeOrderUC *usecase.PlaceOrderUseCase) *OrderController {
	return &OrderController{placeOrderUC: placeOrderUC}
}

func (c *OrderController) PlaceOrder(w http.ResponseWriter, r *http.Request) {
	var req PlaceOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	// Map HTTP request → Use Case input
	items := make([]usecase.PlaceOrderItem, len(req.Items))
	for i, item := range req.Items {
		items[i] = usecase.PlaceOrderItem{
			ProductID: item.ProductID,
			Quantity:  item.Quantity,
			Price:     domain.Money{Amount: item.Price, Currency: "IDR"},
		}
	}

	// Call the Use Case — the controller does not know the implementation behind it
	output, err := c.placeOrderUC.Execute(r.Context(), usecase.PlaceOrderInput{
		CustomerID: req.CustomerID,
		Items:      items,
	})
	if err != nil {
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
		return
	}

	// Map Use Case output → HTTP response
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(PlaceOrderResponse{
		OrderID: output.OrderID,
		Total:   output.Total.Amount,
		Status:  output.Status,
	})
}

// Repository: a concrete implementation accessing the database
package postgres

import (
	"context"
	"database/sql"

	"myapp/domain"
)

// PostgresOrderRepository implements usecase.OrderRepository
// ✓ The interface is defined in the Use Case, the implementation lives here
type PostgresOrderRepository struct {
	db *sql.DB
}

func NewPostgresOrderRepository(db *sql.DB) *PostgresOrderRepository {
	return &PostgresOrderRepository{db: db}
}

func (r *PostgresOrderRepository) Save(ctx context.Context, order *domain.Order) error {
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO orders (id, customer_id, status, total_amount, created_at)
		 VALUES ($1, $2, $3, $4, $5)`,
		order.ID(), order.CustomerID(), order.Status(), order.Total().Amount, time.Now(),
	)
	return err
}

Directory Structure in Go #

A good folder structure mirrors the Clean Architecture layer boundaries:

myapp/
├── domain/                    ← Entities: the most core business rules
│   ├── order.go               ← Order entity + business rules
│   ├── customer.go
│   ├── money.go               ← Value object
│   └── errors.go              ← Domain-specific errors
│
├── usecase/                   ← Use Cases: application business flows
│   ├── place_order.go         ← PlaceOrderUseCase + interfaces
│   ├── cancel_order.go
│   └── get_order.go
│
├── adapter/                   ← Interface Adapters: translators
│   ├── http/
│   │   ├── order_controller.go
│   │   └── router.go
│   ├── grpc/
│   │   └── order_server.go
│   └── dto/
│       └── order_dto.go       ← Structs for the transport layer
│
├── infrastructure/            ← Frameworks & Drivers: implementation details
│   ├── postgres/
│   │   └── order_repository.go
│   ├── kafka/
│   │   └── notification_producer.go
│   └── inventory/
│       └── inventory_client.go
│
└── cmd/                       ← Entry point + dependency injection
    └── server/
        └── main.go
Never import infrastructure from domain or usecase. If you find import "myapp/infrastructure/postgres" inside a domain or use case file, that is a Dependency Rule violation that needs fixing immediately. IDEs like VSCode with the Go plugin can help detect this, or you can use a tool like go-cleanarch to validate it automatically.

Dependency Injection: Assembling All Layers #

Dependency injection is the mechanism that connects all layers without violating the Dependency Rule. The main.go file is the only place allowed to know about all layers at once:

// cmd/server/main.go — the only place that "knows everything"
package main

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

	_ "github.com/lib/pq"

	adapterhttp "myapp/adapter/http"
	"myapp/infrastructure/kafka"
	"myapp/infrastructure/postgres"
	"myapp/usecase"
)

func main() {
	// Initialize infrastructure
	db, err := sql.Open("postgres", "postgres://localhost/myapp?sslmode=disable")
	if err != nil {
		log.Fatal("database connection failed:", err)
	}
	defer db.Close()

	// Create concrete implementations (infrastructure layer)
	orderRepo := postgres.NewPostgresOrderRepository(db)
	inventorySvc := inventory.NewInventoryClient("http://inventory-service")
	notificationSvc := kafka.NewKafkaNotificationProducer("localhost:9092")

	// Create the use case with dependency injection
	// ✓ The use case only receives interfaces — it does not know concrete implementations
	placeOrderUC := usecase.NewPlaceOrderUseCase(orderRepo, inventorySvc, notificationSvc)

	// Create the controller (adapter layer)
	orderController := adapterhttp.NewOrderController(placeOrderUC)

	// Set up the router (framework layer)
	mux := http.NewServeMux()
	mux.HandleFunc("POST /orders", orderController.PlaceOrder)

	log.Println("Server running on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

This dependency injection pattern can be visualized as:

flowchart BT
    PG["postgres.OrderRepository\\n(concrete implementation)"]
    KF["kafka.NotificationProducer\\n(concrete implementation)"]
    IV["inventory.Client\\n(concrete implementation)"]

    UC["PlaceOrderUseCase\\n(only knows interfaces)"]

    CTRL["OrderController\\n(adapter)"]

    MAIN["main.go\\n(wires all dependencies)"]

    PG -->|"injected as OrderRepository"| UC
    KF -->|"injected as NotificationService"| UC
    IV -->|"injected as InventoryService"| UC
    UC -->|"injected into"| CTRL
    MAIN -->|"creates"| PG
    MAIN -->|"creates"| KF
    MAIN -->|"creates"| IV
    MAIN -->|"creates"| UC
    MAIN -->|"creates"| CTRL

Testing: The Main Strength of Clean Architecture #

One of the biggest benefits of Clean Architecture is the ability to test every layer in isolation:

// usecase/place_order_test.go
package usecase_test

import (
	"context"
	"testing"

	"myapp/domain"
	"myapp/usecase"
)

// Mock repository — no real database needed
type mockOrderRepository struct {
	savedOrders []*domain.Order
}

func (m *mockOrderRepository) Save(ctx context.Context, order *domain.Order) error {
	m.savedOrders = append(m.savedOrders, order)
	return nil
}

func (m *mockOrderRepository) FindByID(ctx context.Context, id string) (*domain.Order, error) {
	return nil, nil
}

func (m *mockOrderRepository) FindByCustomer(ctx context.Context, customerID string) ([]*domain.Order, error) {
	return nil, nil
}

// Mock inventory service — no HTTP call to the inventory service needed
type mockInventoryService struct {
	shouldFail bool
}

func (m *mockInventoryService) CheckAndReserve(ctx context.Context, items []domain.OrderItem) error {
	if m.shouldFail {
		return errors.New("insufficient stock")
	}
	return nil
}

// Mock notification service
type mockNotificationService struct{}

func (m *mockNotificationService) NotifyOrderPlaced(ctx context.Context, order *domain.Order) error {
	return nil
}

// Test the use case without a database, without HTTP, without Kafka
func TestPlaceOrder_Success(t *testing.T) {
	repo := &mockOrderRepository{}
	inventory := &mockInventoryService{}
	notification := &mockNotificationService{}

	uc := usecase.NewPlaceOrderUseCase(repo, inventory, notification)

	output, err := uc.Execute(context.Background(), usecase.PlaceOrderInput{
		CustomerID: "customer-123",
		Items: []usecase.PlaceOrderItem{
			{ProductID: "prod-1", Quantity: 2, Price: domain.Money{Amount: 50000, Currency: "IDR"}},
		},
	})

	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}
	if output.OrderID == "" {
		t.Error("expected order ID, got empty string")
	}
	if output.Total.Amount != 100000 {
		t.Errorf("expected total 100000, got %d", output.Total.Amount)
	}
	if len(repo.savedOrders) != 1 {
		t.Errorf("expected 1 saved order, got %d", len(repo.savedOrders))
	}
}

func TestPlaceOrder_InsufficientStock(t *testing.T) {
	repo := &mockOrderRepository{}
	inventory := &mockInventoryService{shouldFail: true} // simulate out of stock
	notification := &mockNotificationService{}

	uc := usecase.NewPlaceOrderUseCase(repo, inventory, notification)

	_, err := uc.Execute(context.Background(), usecase.PlaceOrderInput{
		CustomerID: "customer-123",
		Items: []usecase.PlaceOrderItem{
			{ProductID: "prod-1", Quantity: 999, Price: domain.Money{Amount: 50000, Currency: "IDR"}},
		},
	})

	if err == nil {
		t.Error("expected error for insufficient stock, got nil")
	}
	if len(repo.savedOrders) != 0 {
		t.Error("order must not be saved if stock is insufficient")
	}
}

With mock interfaces, the entire use case can be tested without a single real external dependency — no database, no HTTP calls, no message broker.


Comparison with Layered Architecture #

Clean Architecture and Layered Architecture are often mistaken for the same thing. There is one critical difference:

flowchart LR
    subgraph LA["Layered Architecture"]
        LA_P["Presentation"] --> LA_A["Application"]
        LA_A --> LA_D["Domain"]
        LA_D --> LA_DB["Database"]
        LA_DB_NOTE["Domain depends\\non the Database layer"]
    end

    subgraph CA["Clean Architecture"]
        CA_FW["Frameworks & DB"] --> CA_IA["Interface Adapters"]
        CA_IA --> CA_UC["Use Cases"]
        CA_UC --> CA_EN["Entities"]
        CA_EN_NOTE["Entities depend on\\nnobody"]
    end
AspectLayered ArchitectureClean Architecture
Dependency directionTop-down, domain often depends on the DBAlways inward, domain does not depend on the DB
TestabilityHard — domain tightly coupled to the DBEasy — domain can be tested without a DB
Database positionOften the “center of gravity”An implementation detail that can be swapped
Framework changesCan ripple through all layersOnly touch the outer layer
Learning curveLowHigher
Code overheadLowMore interfaces and mapping

Anti-Patterns to Avoid #

// ✗ Importing a database in a domain entity — Dependency Rule violation
package domain

import "database/sql" // ✗ domain must not know about SQL

type Order struct {
    db *sql.DB // ✗ the entity stores a reference to the database
}

// ✓ Pure domain entity — no infrastructure imports
package domain

type Order struct {
    id     string
    status OrderStatus
    // no database, no HTTP, no framework
}

// ✗ Business logic in the controller
func (c *OrderController) PlaceOrder(w http.ResponseWriter, r *http.Request) {
    // ✗ business validation that should live in the domain
    if order.Total > 10000000 {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    // ✗ direct domain state manipulation from the controller
    order.Status = "confirmed"
}

// ✓ The controller only parses, delegates to the use case, and formats the response
func (c *OrderController) PlaceOrder(w http.ResponseWriter, r *http.Request) {
    var req PlaceOrderRequest
    json.NewDecoder(r.Body).Decode(&req)
    output, err := c.placeOrderUC.Execute(r.Context(), mapToInput(req)) // ✓ delegate
    if err != nil {
        http.Error(w, err.Error(), http.StatusUnprocessableEntity)
        return
    }
    json.NewEncoder(w).Encode(mapToResponse(output))
}

// ✗ Using a database struct directly as an entity
type OrderRow struct {  // ✗ this is a database model, not a domain entity
    ID         int64  `db:"id"`
    CustomerID string `db:"customer_id"`
    Status     string `db:"status"`
}

// ✓ Separate the database model from the domain entity
type Order struct { // domain entity — no db tags
    id         string
    customerID string
    status     OrderStatus
}

type orderRow struct { // database model — only in the infrastructure layer
    ID         int64  `db:"id"`
    CustomerID string `db:"customer_id"`
    Status     string `db:"status"`
}

// ✗ Interfaces that are too large in a use case — violates Interface Segregation
type OrderRepository interface {
    Save(...)
    FindByID(...)
    FindByCustomer(...)
    FindByDateRange(...)      // ✗ only used by one use case
    CountByStatus(...)        // ✗ only used for reporting
    GenerateReport(...)       // ✗ not a repository responsibility
}

// ✓ Small, use-case-specific interfaces
type OrderSaver interface { // only for PlaceOrderUseCase
    Save(ctx context.Context, order *domain.Order) error
}

type OrderFinder interface { // only for GetOrderUseCase
    FindByID(ctx context.Context, id string) (*domain.Order, error)
}

When Not to Use Clean Architecture #

Use Clean Architecture if:
  ✓ The system has complex business rules that will keep growing
  ✓ The system's expected lifetime is more than 2–3 years
  ✓ The team is large enough to need parallel work on different layers
  ✓ Testability is a priority — comprehensive unit tests are needed
  ✓ There is a chance of swapping the database or framework in the future

Consider a simpler approach if:
  ✗ The project is an MVP or prototype that may not continue
  ✗ The domain is very simple — CRUD without complex business rules
  ✗ The team is small (< 3 developers) — the abstraction overhead outweighs the benefit
  ✗ Deadlines are very tight and there is no time for longer initial setup
  ✗ There are no plans to swap the database or add new channels

Clean Architecture Review Checklist #

ENTITIES:
  □ No imports from databases, HTTP, or any framework
  □ Business validation lives inside the entity, not in services or controllers
  □ Getters control access to internal state (no public fields)
  □ Factory functions (NewX) enforce invariants at creation time

USE CASES:
  □ All dependencies are interfaces, not concrete structs
  □ Interfaces are defined in the use case package, not in infrastructure
  □ Input and output use DTOs, not domain entities directly
  □ No imports from HTTP frameworks, databases, or messaging

INTERFACE ADAPTERS:
  □ Controllers contain no business logic
  □ Explicit mapping exists between DTOs and domain objects
  □ Repository implementations satisfy the interfaces defined by use cases
  □ No domain logic in repositories

INFRASTRUCTURE:
  □ Only this layer may import external libraries
  □ All use-case interface implementations live here
  □ Connection pools, retry logic, and error wrapping live here

DEPENDENCY INJECTION:
  □ All dependency wiring happens only in main.go
  □ No global state or singletons inside use cases/domain
  □ DI container usage (wire, fx) is consistent if used

TESTING:
  □ Use case unit tests use mocks, no real database needed
  □ Entities can be tested without any dependencies
  □ Integration tests exist for the repository layer

Summary #

  • One rule defines everything — the Dependency Rule: code in an inner layer must not know about outer layers; dependencies always flow inward toward the domain.
  • Four layers with firm responsibilities — Entities (enterprise business rules), Use Cases (application business rules), Interface Adapters (translators), Frameworks & Drivers (implementation details).
  • The database is an implementation detail — not the center of the system; entities and use cases must not know about or depend on databases, frameworks, or any external library.
  • Interfaces are defined by use cases, not by infrastructure — the use case defines its needs (ports), infrastructure provides the implementations (adapters); this is what makes dependencies flow in the right direction.
  • Controllers contain no business logic — their job is only to parse requests, map to use case inputs, call the use case, and format output; business validation lives in entities or use cases.
  • Transport models are separated from domain entities — do not use database or JSON structs directly as domain entities; use DTOs for the transport layer with explicit mapping in adapters.
  • Testability is a natural result, not an add-on — with interface-based dependency injection, entire use cases can be tested with mocks without a real database, HTTP server, or message broker.
  • main.go is the only place that knows everything — wiring all concrete dependencies into interfaces only happens at the entry point; no other place may know about all layers at once.
  • Best for complex systems with long lifetimes — the abstraction and boilerplate overhead pays off as the system grows; for MVPs or small systems, consider a simpler approach first.
  • It is not a folder template, but a dependency mindset — Clean Architecture is not about having the right folder names; it is about one rule: never import from an outer layer into an inner layer.

← Previous: Overview   Next: Domain-Driven →

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