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.
- Protocol Buffers
- Connect
- gRPC
- Streaming
- REST reverse-proxy
- Clean architecture
- PostgreSQL
- Migration
- 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
make run-server
make run-client
The API definition files (like .proto
).
Non-Go related data. Usually things like images, but here it is test data.
The mains.
The implementation not meant to be used by external systems.
The starting points of the systems. Basically extensions of the mains.
The global configurations.
The entities of the system. Sometimes named as domain.
The package contains definitions of the common types and their methods.
The models. Methods for manipulating models also resides here.
The repository interfaces.
Actual implementation requires communication with the outside world (a database, etc) thus resides in /internal/infrastructure/repository
.
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".
The controllers/handlers of the server.
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.
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.
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 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.
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 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.
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.
- 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.
- Having a struct felt cleaner while providing