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
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!