Implementing Clean Architecture in Go: A Comprehensive Guide
Clean Architecture provides a powerful framework for building applications that are maintainable, testable, and independent of external frameworks and libraries. This comprehensive guide explores implementing Clean Architecture in Go, with practical examples and real-world considerations to help you build robust, future-proof applications.
Implementing Clean Architecture in Go: A Comprehensive Guide
Understanding Clean Architecture
Clean Architecture, proposed by Robert C. Martin (Uncle Bob), is a software design approach that separates concerns by dividing an application into layers. Each layer has specific responsibilities and dependencies flow inward, meaning outer layers can depend on inner layers, but not vice versa.
The primary goals of Clean Architecture are:
- Framework Independence: The core business logic doesn’t depend on any external frameworks
- Testability: Business rules can be tested without external elements like UI, database, or web server
- UI Independence: The UI can change without affecting the rest of the system
- Database Independence: Business rules aren’t bound to a specific database
- External Agency Independence: Business rules don’t know anything about interfaces to the outside world
The Evolution of Architectural Patterns
Clean Architecture builds upon several previous architectural patterns:
- Hexagonal Architecture (Ports and Adapters): Introduced by Alistair Cockburn, it isolates application core from external services
- Onion Architecture: Jeffrey Palermo’s approach emphasizing domain models at the center
- Screaming Architecture: Another concept by Uncle Bob where architecture should “scream” the intent of the system
- DCI (Data, Context, Interaction): Focuses on object roles and interactions
- BCE (Boundary-Control-Entity): Separates concerns into three categories
Clean Architecture synthesizes these ideas into a cohesive approach that emphasizes the separation of concerns and dependency rules.
The Layers of Clean Architecture
Clean Architecture divides an application into concentric layers, with dependencies pointing inward. Let’s explore each layer in detail:

1. Entities (Enterprise Business Rules)
Entities encapsulate enterprise-wide business rules and represent the most stable part of your application. They are the least likely to change when something external changes.
In Go, entities are typically defined as structs with methods:
// domain/post.go
package domain
import "time"
// Post represents a blog post entity
type Post struct {
ID int
Title string
Content string
AuthorID int
PublishedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// IsPublished checks if the post is published
func (p *Post) IsPublished() bool {
return !p.PublishedAt.IsZero() && p.PublishedAt.Before(time.Now())
}
// Validate returns error if the post is invalid
func (p *Post) Validate() error {
if p.Title == "" {
return ErrEmptyTitle
}
if p.Content == "" {
return ErrEmptyContent
}
if p.AuthorID <= 0 {
return ErrInvalidAuthor
}
return nil
}
Entities should:
- Contain critical business data and rules
- Be independent of application-specific logic
- Not be affected by UI, database, or any external framework
- Be the most stable part of your application
2. Use Cases (Application Business Rules)
Use cases implement application-specific business rules. They orchestrate the flow of data to and from entities and direct them to use their enterprise-wide business rules to achieve the goals of the use case.
In Go, use cases are often implemented as “interactors” with the following characteristics:
// usecases/post_interactor.go
package usecases
import (
"context"
"time"
"myapp/domain"
)
// PostUseCase defines the interface for post use cases
type PostUseCase interface {
GetByID(ctx context.Context, id int) (*domain.Post, error)
Create(ctx context.Context, post *domain.Post) error
Update(ctx context.Context, post *domain.Post) error
Delete(ctx context.Context, id int) error
Publish(ctx context.Context, id int) error
}
// PostRepository defines the interface for post data access
type PostRepository interface {
FindByID(ctx context.Context, id int) (*domain.Post, error)
Store(ctx context.Context, post *domain.Post) error
Update(ctx context.Context, post *domain.Post) error
Delete(ctx context.Context, id int) error
}
// PostInteractor implements the PostUseCase interface
type PostInteractor struct {
repository PostRepository
logger Logger
}
// NewPostInteractor creates a new post interactor
func NewPostInteractor(repo PostRepository, logger Logger) PostUseCase {
return &PostInteractor{
repository: repo,
logger: logger,
}
}
// GetByID retrieves a post by ID
func (pi *PostInteractor) GetByID(ctx context.Context, id int) (*domain.Post, error) {
pi.logger.Info("Fetching post with ID", id)
return pi.repository.FindByID(ctx, id)
}
// Create stores a new post
func (pi *PostInteractor) Create(ctx context.Context, post *domain.Post) error {
if err := post.Validate(); err != nil {
pi.logger.Error("Invalid post data", err)
return err
}
post.CreatedAt = time.Now()
post.UpdatedAt = time.Now()
pi.logger.Info("Creating new post", post.Title)
return pi.repository.Store(ctx, post)
}
// Publish makes a post publicly available
func (pi *PostInteractor) Publish(ctx context.Context, id int) error {
post, err := pi.repository.FindByID(ctx, id)
if err != nil {
return err
}
post.PublishedAt = time.Now()
post.UpdatedAt = time.Now()
pi.logger.Info("Publishing post", id)
return pi.repository.Update(ctx, post)
}
// Additional methods for Update and Delete...
Use cases should:
- Implement application-specific business rules
- Orchestrate the flow of data to and from entities
- Be independent of external concerns like databases or UI
- Define interfaces that outer layers must implement
3. Interface Adapters
Interface adapters convert data between the format most convenient for use cases and entities, and the format most convenient for external agencies such as databases or the web.
This layer includes:
- Controllers/Presenters
- Gateways
- Repositories
In Go, they typically look like this:
// interfaces/post_controller.go
package interfaces
import (
"encoding/json"
"net/http"
"strconv"
"myapp/usecases"
)
// PostController handles HTTP requests related to posts
type PostController struct {
postUseCase usecases.PostUseCase
}
// NewPostController creates a new post controller
func NewPostController(useCase usecases.PostUseCase) *PostController {
return &PostController{
postUseCase: useCase,
}
}
// GetPost handles GET requests for a specific post
func (pc *PostController) GetPost(w http.ResponseWriter, r *http.Request) {
// Extract post ID from URL
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid post ID", http.StatusBadRequest)
return
}
// Use the use case to get the post
post, err := pc.postUseCase.GetByID(r.Context(), id)
if err != nil {
http.Error(w, "Post not found", http.StatusNotFound)
return
}
// Convert domain model to response model
response := postResponse{
ID: post.ID,
Title: post.Title,
Content: post.Content,
Published: post.IsPublished(),
CreatedAt: post.CreatedAt,
}
// Return JSON response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// Response model for the controller
type postResponse struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Published bool `json:"published"`
CreatedAt time.Time `json:"created_at"`
}
// Additional methods for Create, Update, Delete...
// interfaces/post_repository.go
package interfaces
import (
"context"
"myapp/domain"
)
// PostRepository implements the usecases.PostRepository interface
type PostRepository struct {
sqlHandler SQLHandler
}
// NewPostRepository creates a new post repository
func NewPostRepository(handler SQLHandler) *PostRepository {
return &PostRepository{
sqlHandler: handler,
}
}
// FindByID retrieves a post by ID from the database
func (pr *PostRepository) FindByID(ctx context.Context, id int) (*domain.Post, error) {
var post domain.Post
row, err := pr.sqlHandler.QueryRow(ctx, "SELECT id, title, content, author_id, published_at, created_at, updated_at FROM posts WHERE id = ?", id)
if err != nil {
return nil, err
}
err = row.Scan(
&post.ID,
&post.Title,
&post.Content,
&post.AuthorID,
&post.PublishedAt,
&post.CreatedAt,
&post.UpdatedAt,
)
if err != nil {
return nil, err
}
return &post, nil
}
// Store saves a new post to the database
func (pr *PostRepository) Store(ctx context.Context, post *domain.Post) error {
query := `
INSERT INTO posts (title, content, author_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
`
result, err := pr.sqlHandler.Exec(
ctx,
query,
post.Title,
post.Content,
post.AuthorID,
post.CreatedAt,
post.UpdatedAt,
)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
post.ID = int(id)
return nil
}
// Additional methods for Update and Delete...
Interface adapters should:
- Convert data between the format used by use cases/entities and the format used by external agencies
- Be responsible for serialization/deserialization
- Handle framework-specific concerns
- Implement interfaces defined by use cases
4. Frameworks and Drivers (External Interfaces)
This outermost layer contains frameworks and tools like the database, the web framework, external APIs, etc. It’s the most volatile part of the application.
In Go, it might look like this:
// infrastructure/sqlhandler.go
package infrastructure
import (
"context"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"myapp/interfaces"
)
// SQLHandler implements the interfaces.SQLHandler interface
type SQLHandler struct {
db *sql.DB
}
// NewSQLHandler creates a new SQL handler
func NewSQLHandler() (*SQLHandler, error) {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true")
if err != nil {
return nil, err
}
return &SQLHandler{db: db}, nil
}
// Execute executes a query without returning any rows
func (handler *SQLHandler) Exec(ctx context.Context, query string, args ...interface{}) (interfaces.Result, error) {
result, err := handler.db.ExecContext(ctx, query, args...)
if err != nil {
return nil, err
}
return result, nil
}
// QueryRow executes a query that returns a single row
func (handler *SQLHandler) QueryRow(ctx context.Context, query string, args ...interface{}) (interfaces.Row, error) {
row := handler.db.QueryRowContext(ctx, query, args...)
return &SQLRow{row}, nil
}
// Query executes a query that returns rows
func (handler *SQLHandler) Query(ctx context.Context, query string, args ...interface{}) (interfaces.Rows, error) {
rows, err := handler.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
return &SQLRows{rows}, nil
}
// SQLRow wraps sql.Row to implement interfaces.Row
type SQLRow struct {
row *sql.Row
}
// Scan copies the values from the row into dest
func (r *SQLRow) Scan(dest ...interface{}) error {
return r.row.Scan(dest...)
}
// Similar implementations for SQLRows and SQLResult...
// infrastructure/router.go
package infrastructure
import (
"net/http"
"myapp/interfaces"
"myapp/usecases"
)
// Router handles HTTP routing
func Router() http.Handler {
// Initialize dependencies
sqlHandler, err := NewSQLHandler()
if err != nil {
panic(err)
}
logger := NewLogger()
// Initialize repositories
postRepo := interfaces.NewPostRepository(sqlHandler)
// Initialize use cases
postUseCase := usecases.NewPostInteractor(postRepo, logger)
// Initialize controllers
postController := interfaces.NewPostController(postUseCase)
// Set up router
mux := http.NewServeMux()
// Register routes
mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
postController.GetPost(w, r)
case http.MethodPost:
postController.CreatePost(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
return mux
}
Frameworks and drivers should:
- Contain all the details and implementation of external tools and frameworks
- Be the most volatile part of the application
- Be easy to replace without affecting the inner layers
- Adapt between the external world and the interfaces defined by the adapter layer
Dependency Inversion Principle in Practice
The Dependency Inversion Principle (DIP) is a crucial concept in Clean Architecture. It states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
In Go, we implement DIP using interfaces. Let’s explore a practical example:
// interfaces/sqlhandler.go
package interfaces
import "context"
// SQLHandler defines the interface for database operations
type SQLHandler interface {
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
QueryRow(ctx context.Context, query string, args ...interface{}) (Row, error)
}
// Result defines the interface for a result of a database operation
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
// Row defines the interface for a single database row
type Row interface {
Scan(dest ...interface{}) error
}
// Rows defines the interface for multiple database rows
type Rows interface {
Next() bool
Scan(dest ...interface{}) error
Close() error
}
In this example:
- The repository in the interface adapter layer depends on the
SQLHandlerinterface, not on concrete implementations. - The concrete implementation of
SQLHandleris in the infrastructure layer. - The direction of dependency is from the outer layer (infrastructure) to the inner layer (interfaces), not the other way around.
This inversion of control allows us to:
- Easily swap out database implementations (MySQL, PostgreSQL, SQLite, etc.)
- Mock the database for testing
- Evolve the database layer without affecting the business logic
Testing in Clean Architecture
One of the major benefits of Clean Architecture is its testability. Here’s how to approach testing for each layer:
Testing Entities
Entities can be tested in isolation without any external dependencies:
// domain/post_test.go
package domain_test
import (
"testing"
"time"
"myapp/domain"
)
func TestPost_IsPublished(t *testing.T) {
tests := []struct {
name string
post domain.Post
want bool
}{
{
name: "Published post",
post: domain.Post{
PublishedAt: time.Now().Add(-24 * time.Hour), // Published yesterday
},
want: true,
},
{
name: "Unpublished post",
post: domain.Post{
PublishedAt: time.Time{}, // Zero time
},
want: false,
},
{
name: "Future publication date",
post: domain.Post{
PublishedAt: time.Now().Add(24 * time.Hour), // Will be published tomorrow
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.post.IsPublished(); got != tt.want {
t.Errorf("Post.IsPublished() = %v, want %v", got, tt.want)
}
})
}
}
func TestPost_Validate(t *testing.T) {
tests := []struct {
name string
post domain.Post
wantErr bool
}{
{
name: "Valid post",
post: domain.Post{
Title: "Test Title",
Content: "Test Content",
AuthorID: 1,
},
wantErr: false,
},
{
name: "Empty title",
post: domain.Post{
Content: "Test Content",
AuthorID: 1,
},
wantErr: true,
},
// Additional test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.post.Validate(); (err != nil) != tt.wantErr {
t.Errorf("Post.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Testing Use Cases
Use cases can be tested by mocking the repository interfaces:
// usecases/post_interactor_test.go
package usecases_test
import (
"context"
"errors"
"testing"
"time"
"myapp/domain"
"myapp/usecases"
)
// MockPostRepository is a mock implementation of the PostRepository interface
type MockPostRepository struct {
posts map[int]*domain.Post
}
func NewMockPostRepository() *MockPostRepository {
return &MockPostRepository{
posts: make(map[int]*domain.Post),
}
}
func (m *MockPostRepository) FindByID(ctx context.Context, id int) (*domain.Post, error) {
post, exists := m.posts[id]
if !exists {
return nil, errors.New("post not found")
}
return post, nil
}
func (m *MockPostRepository) Store(ctx context.Context, post *domain.Post) error {
// Simulate auto-increment ID
if post.ID == 0 {
post.ID = len(m.posts) + 1
}
m.posts[post.ID] = post
return nil
}
// Additional repository methods...
// MockLogger is a mock implementation of the Logger interface
type MockLogger struct{}
func (m *MockLogger) Info(message string, args ...interface{}) {}
func (m *MockLogger) Error(message string, args ...interface{}) {}
func TestPostInteractor_GetByID(t *testing.T) {
// Setup
ctx := context.Background()
repo := NewMockPostRepository()
logger := &MockLogger{}
// Add a test post to the repository
testPost := &domain.Post{
ID: 1,
Title: "Test Post",
Content: "Test Content",
AuthorID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
repo.Store(ctx, testPost)
// Create the interactor
interactor := usecases.NewPostInteractor(repo, logger)
// Test successful retrieval
t.Run("Existing post", func(t *testing.T) {
post, err := interactor.GetByID(ctx, 1)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if post.ID != 1 || post.Title != "Test Post" {
t.Errorf("Got incorrect post: %+v", post)
}
})
// Test post not found
t.Run("Non-existent post", func(t *testing.T) {
_, err := interactor.GetByID(ctx, 999)
if err == nil {
t.Error("Expected error, got nil")
}
})
}
// Additional test functions for Create, Update, Publish, etc.
Testing Controllers
Controllers can be tested using the standard Go HTTP testing utilities:
// interfaces/post_controller_test.go
package interfaces_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"myapp/domain"
"myapp/interfaces"
"myapp/usecases"
)
// MockPostUseCase is a mock implementation of the PostUseCase interface
type MockPostUseCase struct {
posts map[int]*domain.Post
}
func NewMockPostUseCase() *MockPostUseCase {
return &MockPostUseCase{
posts: map[int]*domain.Post{
1: {
ID: 1,
Title: "Test Post",
Content: "Test Content",
AuthorID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
}
}
func (m *MockPostUseCase) GetByID(ctx context.Context, id int) (*domain.Post, error) {
post, exists := m.posts[id]
if !exists {
return nil, errors.New("post not found")
}
return post, nil
}
// Additional use case methods...
func TestPostController_GetPost(t *testing.T) {
// Setup
useCase := NewMockPostUseCase()
controller := interfaces.NewPostController(useCase)
// Test successful retrieval
t.Run("Existing post", func(t *testing.T) {
req, err := http.NewRequest("GET", "/posts?id=1", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(controller.GetPost)
handler.ServeHTTP(rr, req)
// Check status code
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
// Check response body
var response map[string]interface{}
err = json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}
if id, ok := response["id"].(float64); !ok || int(id) != 1 {
t.Errorf("Expected post ID 1, got %v", response["id"])
}
if title, ok := response["title"].(string); !ok || title != "Test Post" {
t.Errorf("Expected title 'Test Post', got %v", response["title"])
}
})
// Test post not found
t.Run("Non-existent post", func(t *testing.T) {
req, err := http.NewRequest("GET", "/posts?id=999", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(controller.GetPost)
handler.ServeHTTP(rr, req)
// Check status code
if status := rr.Code; status != http.StatusNotFound {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusNotFound)
}
})
// Test invalid ID
t.Run("Invalid ID", func(t *testing.T) {
req, err := http.NewRequest("GET", "/posts?id=invalid", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(controller.GetPost)
handler.ServeHTTP(rr, req)
// Check status code
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
}
})
}
// Additional test functions for CreatePost, UpdatePost, etc.
Advanced Patterns and Best Practices
Error Handling
Error handling in Clean Architecture should respect the separation of concerns:
// domain/errors.go
package domain
import "errors"
// Domain-specific errors
var (
ErrEmptyTitle = errors.New("title cannot be empty")
ErrEmptyContent = errors.New("content cannot be empty")
ErrInvalidAuthor = errors.New("author ID must be positive")
)
// usecases/errors.go
package usecases
import "errors"
// Use case specific errors
var (
ErrPostNotFound = errors.New("post not found")
ErrUnauthorized = errors.New("user not authorized to perform this action")
)
// interfaces/errors.go
package interfaces
import (
"errors"
"net/http"
)
// Error represents an error with an associated HTTP status code
type Error struct {
Err error
StatusCode int
}
func (e *Error) Error() string {
return e.Err.Error()
}
// NewError creates a new Error
func NewError(err error, statusCode int) *Error {
return &Error{
Err: err,
StatusCode: statusCode,
}
}
// Error handling in controllers
func (pc *PostController) GetPost(w http.ResponseWriter, r *http.Request) {
// ... (previous code)
post, err := pc.postUseCase.GetByID(r.Context(), id)
if err != nil {
switch {
case errors.Is(err, usecases.ErrPostNotFound):
http.Error(w, "Post not found", http.StatusNotFound)
case errors.Is(err, usecases.ErrUnauthorized):
http.Error(w, "Unauthorized", http.StatusUnauthorized)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// ... (rest of the handler)
}
Handling Database Transactions
Transactions often cut across multiple operations and should be managed at the use case level:
// interfaces/sqlhandler.go (extended)
package interfaces
import "context"
// SQLHandler defines the interface for database operations
type SQLHandler interface {
// Previous methods...
// Transaction handling
BeginTx(ctx context.Context) (Tx, error)
}
// Tx represents a database transaction
type Tx interface {
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
QueryRow(ctx context.Context, query string, args ...interface{}) (Row, error)
Commit() error
Rollback() error
}
// usecases/post_interactor.go (extended)
func (pi *PostInteractor) CreateWithTransaction(ctx context.Context, post *domain.Post) error {
if err := post.Validate(); err != nil {
return err
}
post.CreatedAt = time.Now()
post.UpdatedAt = time.Now()
// Begin transaction
tx, err := pi.repository.BeginTx(ctx)
if err != nil {
return err
}
// Ensure transaction is rolled back on error
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Store the post
err = pi.repository.StoreWithTx(ctx, tx, post)
if err != nil {
return err
}
// Perform additional operations within the transaction
// ...
// Commit the transaction
return tx.Commit()
}
Input Validation
Input validation should primarily occur at the controller level before passing data to the use cases:
// interfaces/post_controller.go (extended)
type createPostRequest struct {
Title string `json:"title"`
Content string `json:"content"`
AuthorID int `json:"author_id"`
}
func (pc *PostController) CreatePost(w http.ResponseWriter, r *http.Request) {
// Parse and validate input
var req createPostRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Perform validation
var validationErrors []string
if req.Title == "" {
validationErrors = append(validationErrors, "title is required")
}
if req.Content == "" {
validationErrors = append(validationErrors, "content is required")
}
if req.AuthorID <= 0 {
validationErrors = append(validationErrors, "author_id must be positive")
}
if len(validationErrors) > 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"errors": validationErrors,
})
return
}
// Create domain entity
post := &domain.Post{
Title: req.Title,
Content: req.Content,
AuthorID: req.AuthorID,
}
// Call use case
err = pc.postUseCase.Create(r.Context(), post)
if err != nil {
// Error handling as before
return
}
// Return response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"id": post.ID,
"message": "Post created successfully",
})
}
Handling Dependencies
Managing dependencies can become complex in larger applications. Consider using a dependency injection container:
// infrastructure/container.go
package infrastructure
import (
"myapp/interfaces"
"myapp/usecases"
)
// Container manages application dependencies
type Container struct {
sqlHandler interfaces.SQLHandler
logger usecases.Logger
// Repositories
postRepository interfaces.PostRepository
// Use cases
postUseCase usecases.PostUseCase
// Controllers
postController *interfaces.PostController
}
// NewContainer creates a new dependency container
func NewContainer() (*Container, error) {
container := &Container{}
// Initialize infrastructure
sqlHandler, err := NewSQLHandler()
if err != nil {
return nil, err
}
container.sqlHandler = sqlHandler
logger := NewLogger()
container.logger = logger
// Initialize repositories
container.postRepository = interfaces.NewPostRepository(sqlHandler)
// Initialize use cases
container.postUseCase = usecases.NewPostInteractor(container.postRepository, logger)
// Initialize controllers
container.postController = interfaces.NewPostController(container.postUseCase)
return container, nil
}
// GetPostController returns the post controller
func (c *Container) GetPostController() *interfaces.PostController {
return c.postController
}
// Close cleans up resources
func (c *Container) Close() error {
// Close any resources that need cleanup
return nil
}
// infrastructure/router.go (updated)
func Router() (http.Handler, func() error) {
// Initialize dependencies
container, err := NewContainer()
if err != nil {
panic(err)
}
// Set up router
mux := http.NewServeMux()
// Get controllers
postController := container.GetPostController()
// Register routes
mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
postController.GetPost(w, r)
case http.MethodPost:
postController.CreatePost(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// Return router and cleanup function
return mux, container.Close
}
Real-World Considerations
Project Structure
A typical Go project using Clean Architecture might be structured like this:
myapp/
├── cmd/
│ └── server/
│ └── main.go
├── domain/
│ ├── errors.go
│ ├── post.go
│ └── user.go
├── infrastructure/
│ ├── container.go
│ ├── env.go
│ ├── logger.go
│ ├── router.go
│ └── sqlhandler.go
├── interfaces/
│ ├── errors.go
│ ├── post_controller.go
│ ├── post_repository.go
│ ├── sqlhandler.go
│ ├── user_controller.go
│ └── user_repository.go
├── usecases/
│ ├── errors.go
│ ├── logger.go
│ ├── post_interactor.go
│ ├── post_repository.go
│ ├── user_interactor.go
│ └── user_repository.go
├── go.mod
└── go.sum
Migration Strategies
Migrating an existing application to Clean Architecture can be challenging. Here’s a pragmatic approach:
Identify Core Business Logic: Identify the core business logic that should be in the Entity and Use Case layers.
Create Facades: Create facades that abstract away implementation details of existing code:
// Facade over an existing service
type LegacyServiceFacade struct {
legacyService *LegacyService
}
func (f *LegacyServiceFacade) DoSomething(ctx context.Context, input *domain.Input) (*domain.Output, error) {
// Convert domain model to legacy model
legacyInput := convertToLegacyInput(input)
// Call legacy service
legacyOutput, err := f.legacyService.Process(legacyInput)
if err != nil {
return nil, err
}
// Convert legacy output to domain model
output := convertFromLegacyOutput(legacyOutput)
return output, nil
}
Implement Interfaces: Implement the interfaces defined by your Use Case layer using the facades.
Gradually Replace: As you gain confidence, gradually replace the legacy implementation with clean implementations.
Incremental Adoption: Consider adopting Clean Architecture for new features while maintaining the existing architecture for old features.
Performance Considerations
Clean Architecture can introduce overhead due to additional layers and abstractions. Consider these performance optimizations:
Caching: Implement caching at the appropriate layer, typically at the repository level.
Bulk Operations: Support bulk operations to reduce the number of database calls.
DTO Optimization: Optimize DTOs (Data Transfer Objects) to minimize serialization/deserialization overhead.
Avoid Excessive Abstractions: Don’t create abstractions that don’t provide real benefits.
Profile and Benchmark: Regularly profile and benchmark your application to identify bottlenecks.
Monitoring and Observability
Clean Architecture doesn’t prevent you from implementing proper monitoring and observability:
// infrastructure/monitoring.go
package infrastructure
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// TracingMiddleware adds tracing to HTTP handlers
func TracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tracer := otel.GetTracerProvider().Tracer("myapp/http")
ctx, span := tracer.Start(ctx, r.URL.Path, trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
// Add common attributes
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
)
// Create a wrapped response writer to capture status code
rw := newResponseWriter(w)
// Execute the handler with the trace context
start := time.Now()
next.ServeHTTP(rw, r.WithContext(ctx))
duration := time.Since(start)
// Record response details
span.SetAttributes(
attribute.Int("http.status_code", rw.statusCode),
attribute.Float64("http.response_time_ms", float64(duration.Milliseconds())),
)
})
}
// Similar implementations for database tracing, metrics collection, etc.
Conclusion
Clean Architecture provides a powerful approach to building maintainable, testable, and framework-independent applications. By separating concerns and enforcing dependency rules, it creates a codebase that can evolve with changing requirements and technologies.
While Clean Architecture does introduce some complexity and overhead, the benefits of improved maintainability, testability, and flexibility often outweigh these costs, especially for complex applications with long lifespans.
Remember that Clean Architecture is a set of principles, not a rigid framework. Adapt it to your specific needs and constraints, and don’t be afraid to make pragmatic compromises when necessary.
For Go developers, Clean Architecture aligns well with the language’s emphasis on simplicity, explicitness, and interfaces. By leveraging Go’s strengths, you can create clean, maintainable, and efficient applications that stand the test of time.
Note: The code examples in this article are simplified for clarity and may not cover all edge cases or best practices. Always adapt the principles to your specific requirements and environment.