Clean-Protobuf
A heavily modified, clean architecture version of https://github.com/grpc/grpc-go/tree/master/examples/route_guide.
The project layout is based on https://github.com/golang-standards/project-layout.
The architecture is influenced by https://github.com/evrone/go-clean-template, and a little bit https://github.com/Creatly/creatly-backend.
Features
- Protocol Buffers
- Connect
- gRPC
- Streaming
- REST reverse-proxy
- Clean architecture
- PostgreSQL
- Generics
- Code generation
- gRPC servers (protoc)
- GORM models (gorm.io/gen)
- Initial user defined models (template)
- Repository interfaces (template)
- Initial repository implementation (template)
- OpenAPI v3 (protoc-gen-openapi)
- Containers
- De facto standard layout
Getting Started
make run-server
make run-client
Layout
/api
The API definition files (like .proto
).
/assets
Non-Go related data. Usually things like images, but here it is test data.
/cmd
The mains.
/internal
The implementation not meant to be used by external systems.
/internal/app
The starting points of the systems. Basically extensions of the mains.
/internal/config
The global configurations.
/internal/entity
The entities of the system. Sometimes named as domain.
The package contains definitions of the common types and their methods.
/internal/entity/model
The models. Methods for manipulating models also resides here.
/internal/entity/repository
The repository interfaces.
Actual implementation requires communication with the outside world (a database, etc) thus resides in /internal/infrastructure/repository
.
/internal/infrastructure
The implementation which directly communicates with the outside world and does conversion into entities.
Basically gateways/mediators between the business logic (use cases) and the outside world that translates "their" data into "ours".
/internal/infrastructure/controller
The controllers/handlers of the server.
/internal/infrastructure/repository
The database logic. It reads/writes data from/to the databases or alike. The structs here implement interfaces in /internal/entity/repository
.
You can have multiple implementation here to support different versions, mocks, different ORMs, anything.
/internal/pkg
The internal packages. The generated API code resides here.
The packages are independent of the rest of the implementation.
Tools that only have specific internal purposes.
/internal/usecase
The business logic. The part which is not "chores".
It does not have direct external dependencies like database or API definitions. All those dependencies must be abstracted to be used here.
Tools that can be configured and reused.
Code Generation
Code generation is used for:
- gRPC servers (protoc)
- GORM models (gorm.io/gen)
- initial user defined models (template)
- repository interfaces (template)
- initial repository implementation (template)
They are all programmatically generated by /tools/db-code-generator
, so you can modify the code or templates for customization.
To re-generate code, run the following command:
make generate-db-code
Models and repositories use generated models or generics promoted to minimize the amount of code to be hand-written.
It does not affect GORM behaviors.
Model Customization
If you want to add fields that do not exist on the database for your convenience, the following would work:
type Feature struct {
generated.Feature
AdditionalField string `gorm:"-"`
}
Other GORM tags should work similarly.
Use Case Callbacks
Use cases call a callback function when an important event occurs. Callbacks act as output ports, abstraction of the outside world, and thus are supposed to be implemented and provided by the infrastructure layer.
This style is rather rigorous and cumbersome. Moreover, majority of the times, it can be replaced by simple return values. However, it provides a lot more flexibility to the infrastructure layer. The examples are shown in the streaming RPC controllers where we still achieve practically one-to-one port from the original single handler implementation to infrastructure-usecase-entity implementation.
Database Migration
To add a new migration in one command, run the following
make create-new-postgres-migration NAME=new_migration_file_name
To migrate up/down, use one of the make migrate-*
targets.
Other differences from the Original
- Use PostgreSQL as the data store instead of a slice and a map.
RouteChat
(route.PostMessage
) does not use a transaction to make saving a new message and reading existing messages atomic while the original is.
- Although the behavior is slightly different, there's no practical disadvantage (your new message might not be the latest message, but who cares?)
- Use environment variables instead of flags.
- Having a struct felt cleaner while providing
/internal/config
example.