Picture this: you’re building an API that needs to outlive framework trends, survive database migrations, and withstand the inevitable “let’s rewrite everything in Rust” meetings. Welcome to Clean Architecture in Go - where we separate concerns like diplomats dividing contested territory. Today, we’ll craft a TODO API that’s more organized than my grandma’s spice rack.

Laying the Foundation

Start by creating our project skeleton:

go mod init github.com/yourname/todo-clean

Now let’s install our digital survival kit:

go get github.com/gorilla/mux github.com/jmoiron/sqlx \
github.com/lib/pq github.com/kelseyhightower/envconfig

Our directory structure will look like this:

todo-clean/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── entity/
│   ├── usecase/
│   ├── delivery/
│   └── repository/

This structure keeps our code as organized as a Tetris champion’s apartment.

The Clean Layers Revealed

graph TD A[HTTP Request] --> B[Delivery Layer] B --> C[Use Case Layer] C --> D[Repository Layer] D --> E[PostgreSQL] E --> D D --> C C --> B B --> A

1. Entity Layer - Our Business DNA

In entity/todo.go:

package entity
type Todo struct {
    ID          string `json:"id" db:"id"`
    Title       string `json:"title" validate:"required"`
    Description string `json:"description"`
    Completed   bool   `json:"completed"`
}
type TodoRepository interface {
    Store(todo *Todo) error
    FindByID(id string) (*Todo, error)
    Delete(id string) error
}

These entities are the constitution of our application - they don’t care about HTTP or databases, just business rules.

2. Use Case Layer - The Brain

In usecase/todo.go:

package usecase
type TodoInteractor struct {
    repo entity.TodoRepository
}
func (ti *TodoInteractor) CreateTodo(t entity.Todo) error {
    if t.Title == "" {
        return errors.New("title cannot be empty")
    }
    return ti.repo.Store(&t)
}
// Add other CRUD methods

This is where business logic lives, completely framework-agnostic. It’s like teaching your dog commands - the dog doesn’t care if you’re speaking English or Klingon.

3. Delivery Layer - The Messenger

In delivery/http/handler.go:

package http
func NewTodoHandler(usecase usecase.TodoInteractor) *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/todos", createTodoHandler(usecase)).Methods("POST")
    // Add other routes
    return r
}
func createTodoHandler(uc usecase.TodoInteractor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var todo entity.Todo
        json.NewDecoder(r.Body).Decode(&todo)
        if err := uc.CreateTodo(todo); err != nil {
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
            return
        }
        w.WriteHeader(http.StatusCreated)
    }
}

Our HTTP handlers are just translators - converting HTTP-speak to business logic commands.

4. Repository Layer - The Shape-Shifter

In repository/postgres/todo.go:

package postgres
type TodoRepo struct {
    db *sqlx.DB
}
func (tr *TodoRepo) Store(t *entity.Todo) error {
    _, err := tr.db.NamedExec(`
        INSERT INTO todos (id, title, description, completed)
        VALUES (:id, :title, :description, :completed)`, t)
    return err
}
// Implement other repository methods

Swapping databases is like changing tires - the car (business logic) keeps running.

Wiring It All Together

In cmd/api/main.go:

func main() {
    var cfg config.AppConfig
    envconfig.MustProcess("TODO", &cfg)
    db := sqlx.MustConnect("postgres", cfg.DatabaseURL)
    defer db.Close()
    todoRepo := repository.NewPostgresTodoRepo(db)
    todoUseCase := usecase.NewTodoInteractor(todoRepo)
    handler := delivery.NewTodoHandler(todoUseCase)
    log.Printf("Starting server on %s", cfg.ServerAddress)
    log.Fatal(http.ListenAndServe(cfg.ServerAddress, handler))
}

This main function is the puppet master coordinating all our layers.

Testing: Our Safety Net

func TestTodoCreation(t *testing.T) {
    mockRepo := new(MockTodoRepo)
    useCase := usecase.NewTodoInteractor(mockRepo)
    t.Run("valid todo", func(t *testing.T) {
        err := useCase.CreateTodo(entity.Todo{Title: "Test"})
        assert.Nil(t, err)
    })
    t.Run("empty title", func(t *testing.T) {
        err := useCase.CreateTodo(entity.Todo{Title: ""})
        assert.ErrorContains(t, err, "cannot be empty")
    })
}

Our tests prove that business rules hold even if we switch to MongoDB tomorrow.

The Payoff: Flexibility FTW

Need to add gRPC? Just create a new delivery implementation. Want to cache todos? Create a repository decorator. It’s like LEGO for backend developers. So there you have it - a Clean Architecture in Go that’s more maintainable than a Tesla. Remember, good architecture isn’t about following trends, it’s about building systems that survive your future self’s bad decisions. Now go forth and organize those codebases like you’re Marie Kondo-ing a startup!