Technical Article
Idiomatic backend structure in Go
Use clear package boundaries, small interfaces, and explicit dependencies to keep a Go backend easy to trace.
Begin with the request path
A backend is easier to understand when you can trace one request from the router to the handler, through the service layer, into persistence, and back out as a response.
That path should not jump through unnecessary abstractions. Each layer should add a specific kind of work: HTTP concerns, business rules, data access, or shared domain shape.
Use internal for application code
The internal directory is a useful default for code that belongs to this application and should not be imported by other projects. It makes the boundary explicit without needing a custom rule.
A small service might put routing, handlers, services, repositories, config, and domain models under internal. The cmd directory can stay focused on wiring and startup.
backend/
cmd/api/main.go
internal/app/app.go
internal/handler/gallery.go
internal/service/gallery.go
internal/repository/gallery.go
internal/model/photo.go
internal/router/router.goKeep handlers thin
Handlers should translate HTTP into application calls. They parse input, call a service, handle errors, and write a response. They should not quietly become the place where business rules live.
Thin handlers make testing easier because the interesting behavior moves into regular Go methods that do not need a fake HTTP request to run.
Put business rules in services
Services coordinate the actual application behavior. They decide what needs to be validated, which repository methods are called, and what errors mean in product terms.
A clear service with explicit dependencies is often enough structure for a production backend.
Let repositories hide persistence details
A repository should express storage operations in domain language instead of leaking database or API details everywhere else. The service should ask for photos, users, posts, or orders; it should not care whether they came from Postgres, S3, Google Drive, or a cache.
Keep the interface small. If one service only needs FindByID, do not force it to depend on an entire database client.
Wire dependencies once
Application startup is where dependencies should come together: config, clients, repositories, services, handlers, and router. Keeping that wiring in one place makes runtime behavior easier to inspect.
Constructors that accept concrete dependencies keep the setup readable, testable, and easy to follow.
Backend checklist
Trace one request from router to response before adding new layers.
Keep HTTP parsing in handlers and business rules in services.
Hide persistence details behind small repository methods.
Wire dependencies in startup code instead of constructing them deep inside handlers.