A Clean Architecture microservice template written in Go.
For version 3 of Catalyst, my main focus was to make it simple, clean and upgradable. Looking back, these are the very things I struggled with in both previous versions. Especially upgradability.
I have removed a substantial amount of code that I had passionately written for Catalyst over the years. In hindsight, I realized that I was just reinventing the wheel again and again, while better alternatives already existed. The more code I added for things like dynamic IoC container resolution, generalized DB transactions, and even logging and metric generation, the more opinionated Catalyst became.
With all that bulk gone, Catalyst has become more of a template. Now, instead of dictating which resources to use, Catalyst simply defines where each type of resource should go. As the developer, you have the freedom to choose whatever you feel best fits your implementation.
For example, you can use a simple struct implementation as the IoC container and resolve it manually. This is what I’ve done here for demo purposes (and also what I would actually do unless there’s a very good reason to opt for something else). Alternatively, you can plug in a heavy-duty IoC container. The same applies to loggers, database adapters, and so on.
I’m maintaining a separate GitHub repository at kosatnkn/catalyst-pkgs to host some of the packages I use here. The logger is a wrapper around rs/zerolog, and the configuration parser is a wrapper around spf13/viper. Feel free to swap them out for whatever better suits your needs.
There are many ways to organize a project that follows the Clean Architecture paradigm. This is how I’ve organized Catalyst.
When this architecture is mapped to the directory structure of Catalyst, it looks like this.
The Domain contains all the business logic executed by the microservice. It consists of three main parts: Entities, Use Cases, and Boundary.
Entities define the data model for the domain. These are simple Go structs used within the domain as well as across the domain boundary to transfer data.
Usecases contain all the business logic. Any external dependencies needed by the Use Cases (e.g., database resources) are injected into them using dependency inversion.
The Boundary marks the interface between the Domain and the orchestration layers. It contains contracts (Go interfaces) that facilitate dependency inversion.
Orchestration contains Infrastructure, Presentation and Persistence.
Infrastructure contains the lowest-level resources needed by the microservice, such as configuration and the IoC container.
Presentation contains all outward-facing interfaces. These are the communication channels between the microservice and the outside world. This is where you place your RESTful, GraphQL, gRPC, or WebSocket servers. It’s worth noting that you don’t need to implement all of these in a single microservice; it solely depends on the specifics of your implementation.
Telemetry configurations for metrics and traces can be set up here as well. However, with currently available options, I would use an eBPF collector to gather telemetry. Unless you need to export custom metrics from your service, this approach provides sufficient information about your service.
Persistence is used to hold all data-related resources, whether it’s simple file writes, an RDBMS, an object store, or even an event-sourcing system backed by a local store. The important thing to remember is that all implementation details should be encapsulated within the Persistence layer. The Domain using these resources must not know (or care) about how persistence is implemented. Saving to a static file should be no different than saving to a messaging backend from the perspective of the Domain layer. All complexities related to the underlying persistence technologies should remain contained within the Persistence layer.
Catalyst comes with a script that makes it easy to create new projects from it. You can find this script included with each release. It is version-locked to that specific release.
Use the following command to directly create a new microservice using Catalyst in your current working directory.
curl -fsSL https://github.com/kosatnkn/catalyst/releases/download/v3.2.0/new_from_v3.2.0.sh | bash -s -- --module="example.com/dummyuser/sampler" --yesIf you prefer to first download the script, inspect it, and then run it (which is the safer approach), use following commands.
# download first
curl -fsSL -o new_from_v3.2.0.sh https://github.com/kosatnkn/catalyst/releases/download/v3.2.0/new_from_v3.2.0.sh
# inspect
# ...
# once ready, run
chmod +x new_from_v3.2.0.sh
./new_from_v3.2.0.sh --module="example.com/dummyuser/sampler"NOTE:
The directory name for your new microservice will be inferred from the Go module name that you provide with the
--moduleparameter.The script can handle version information in the module name when inferring the directory name. For example, both
example.com/dummyuser/samplerandexample.com/dummyuser/sampler/v2will producesampleras the directory name.
The best place to start how a microservice based on Catalyst works is the main.go file and the main function.
You can easily identify how the microservice starts up and shuts down by going through the main function in execution order.
By default it will,
- Parse all configurations in to the
infra.Configstruct. - Resolve the container.
- Start the Presentation layer. This includes the
RESTserver, aWebSocketserver etc. - Block the
mainfunction using a channel that waits for interrupt signals.
You can provide configurations in one of two ways. Using a config.yaml file or using environment variables.
You can start by using the config.yaml.example file to create the basic config.yaml file. The current setup expects the config file (if there is a one) to be named as config.yaml.
The structure of the config.yaml file depends on the structure of the Config struct in ./infra/config.go file.
NOTE: When using environment variables it will override values read from the
config.yamlfile.
To avoid conflicts environment variables used are prefixed. The prefix to use is configurable (see config.Settings used in the main function in main.go).
Names of environment variables too depends on the structure of the Config struct in ./infra/config.go file. You can find an example usage in the Makefiles run-with-env section.
Basic set of metadata such as build info for the microservice can be obtained using the metadata package that comes with the microservice.
You will have to use make build or make run commands from the Makefile to have these metadata properly read and loaded.
If you need to add some additional static metadata, you can put them in the ./metadata.txt file.
A Makefile is used to streamline run, build, test and dependency update workflows.