A REST API for restaurant reservations made in Go. Handles full booking workflow: client registration, table availability, payment processing, reservation creation, visit confirmation, and automatic refunds.
A Project to explore Clean Architecture principles, strict separation of concerns, dependency injection and Go patterns.
This codebase is organized with Clean Architecture notions and feature driven modular organization for modules. I left a description of each folder below along with some decision mini-notes.
cmd/api/: HTTP server entry point. Contains everything related to the API binary. Handlers are stored here and not ininternal/because those are specific adapters of this binary.main.go: App entry point. Main file.routes.go: Definition and route register.container.go: Dependency Injection. Initializes repositories, services, usecases, handlers, and it wires them together.handlers/: HTTP controllers. The only concern this directory follows is to receive http requests, call the business layer and return a http response. Does not contain business logic.
configs/: Loads environmental variables. All env variables goes through here. It's the only file that should do this.infrastructure/: External dependencies implementations. They are outsideinternal/because by definition contains swappable adapters (Supabase, Stripe, etc.).internal/is made for inmutable business logic,Infrastructureis made for this reason.core/: Utilities used in crosswide infrastructure, JWT and Rate limiting for example. They live ininfrastructure/and not ininternal/because depends on external libraries.database/: Postgress based pgxpool connection and specific implementations for repositories (repos/).payments/: Payment provider implementation. Currently it is a mock that implemments a interface that should satisfy the app needs
internal/: business logic / Domain rules layer. Whatever is inside this directory cannot be imported for external modules outside this project.modules/: Independent domain modules (such as payment, reservation, staff, etc). Each module is atomic and contains structs (models.go), interfaces (repository.go) and their logic business (service.go). These modules cannot import other modules. (I mean 'atomic' in the sense of independency and self-sufficience. If it's deleted, no other module should have a problem with it).shared/: Shared utilities: Standar structured responses, pagination, validators, etc.usecases/: Flow orchestrators that involves two or more modules. If a endpoint requires one module, the handler can call it directly without going through a use case.
| Layer | What it knows | What it does NOT know |
|---|---|---|
| cmd/api/handlers | HTTP, gin, JSON | business logic/rules |
| internal/usecases | full flow, modules | HTTP, DB |
| modules/service | it's own domain rules | Other modules and infrastructure |
| modules/repository | Interface | business logic/rules |
| infrastructure | Stripe, Supabase, etc | internal logic |
cmd/api/ => Can import everything (internal + infrastructure).
infrastructure/ => Can import 'internal and modules'
infrastructure/ => Can import other directories and modules of infrastructure
infrastructure/ => Must not import cmd
internal/modules/ => Must not import infrastructure
internal/modules/ => Must not improt another modules
internal/modules/ => Must not import usecases
internal/usecases => Can import modules
Important questioning to define this limits: "If i delete a module, can only the usecases/handlers fail explicitly?" if anything else breaks, this limits are not well defined.
Some decisions i would like to remark.
Do not share the same struct with all tags across layers.
- Domain (
models.go): No tags, pure type. Circulates throughout the app - DB (
infrastructure/database/repos/): Unexported struct withdb:""tags. Specific to the database. - HTTP (
handlers/):request/responsestructs (also unexported) withjson:""tags. Specific to the handler
And by that means, a module interface should only be declared with it's domain types. Example:
// Do. Use the struct defined in the module itself.
type Repository interface {
ListAll(ctx context.Context) ([]Table, error)
}
// Do not. DBTables should only exist in `database/repos`
type Repository interface {
ListAll(ctx context.Context) ([]DBTables, error)
}// infrastructure/database/repos/tables.go
type repoImpl struct { ... } // nothing outside this module can modify the declaration.
// Do. Expose the interface and not the implementation obj. (plus: it's easier to wire too).
func NewTableRepo(db *pgxpool.Pool) tables.Repository
// Do not. Exposes the implementation obj and breaks encapsulation.
func NewTableRepo(db *pgxpool.Pool) *repoImplcontainer.go is the only authorized layer to wire all the modules, repositories and external implementations.
This way also makes all dependency errors shows in compilation time and it's easier to test with mocks (i believe).
func BuildContainer(pool *pgxpool.Pool) *Container {
// infrastructure recives the pool
tableRepo := repos.NewTableRepo(pool) // injects the db implementation and returns interface tables.Repository
tableService := tables.NewService(tableRepo) // receives that interface
return &Container{
Tables: handlers.NewTableHandler(tableService), // uses the service injected with the pool conn
}
}(and this somehow also explains a little bit how the file system structure is related to the layers in sequential order)
internal/modules/{modulo}/models.go: Define domain entities (no tags structs. pure types).internal/modules/{modulo}/repository.go: Define the interfaces for the functions implementation.internal/modules/{modulo}/service.go: Define business logic using the interface declared previously.infrastructure/database/repos/{modulo}.go: Define / implemment the interface with real SQL. Use unexported specific structs for gathering db info. The constructor defines the interface, not the type. Alredy discussed this above.internal/usecases/: (Optional) If a feature/endpoint requires more than two modules, implemment a usecase for it. otherwise omit this.cmd/api/handlers/{modulo}.go: Define the HTTP requests and responses, call the usecase/module and return the data.cmd/api/container.goyroutes.go: Inject dependencies and make a route/group for it.
All endpoints except for staff login requires a valid JWT token in the Authorization header (Bearer <token>).
Just login as a staff member and add header Authorization with value Bearer token to accede the other endpoints.
| Method | Endpoint | Description |
|---|---|---|
| General | ||
| GET | / |
Root endpoint showing all registered routes |
| Staff | ||
| POST | /v1/staff/login |
Staff login to obtain a JWT token |
| Tables | ||
| GET | /v1/tables/ |
List all tables |
| GET | /v1/tables/:id |
Find a table by ID (includes availability) |
| Users | ||
| GET | /v1/users/ |
List all users |
| POST | /v1/users/ |
Create a new user |
| GET | /v1/users/:uuid |
Find a user by UUID |
| DELETE | /v1/users/:uuid |
Delete a user by UUID |
| Reservations | ||
| POST | /v1/reservations/ |
Create a new reservation |
| GET | /v1/reservations/ |
Get reservations by client (uses query param ?client_id=:uuid) |
| GET | /v1/reservations/:uuid |
Get reservation by UUID |
| PATCH | /v1/reservations/:uuid |
Update reservation details |
| POST | /v1/reservations/:uuid/cancel |
Cancel a reservation |
| POST | /v1/reservations/:uuid/refund |
Process a refund for a reservation |