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:#1b4332Arrows 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 importinfrastructurefromdomainorusecase. If you findimport "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 likego-cleanarchto 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"| CTRLTesting: 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| Aspect | Layered Architecture | Clean Architecture |
|---|---|---|
| Dependency direction | Top-down, domain often depends on the DB | Always inward, domain does not depend on the DB |
| Testability | Hard — domain tightly coupled to the DB | Easy — domain can be tested without a DB |
| Database position | Often the “center of gravity” | An implementation detail that can be swapped |
| Framework changes | Can ripple through all layers | Only touch the outer layer |
| Learning curve | Low | Higher |
| Code overhead | Low | More 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.