Example Go monolith with embedded microservices and The Clean Architecture
This project shows an example of how to implement monolith with embedded microservices (a.k.a. modular monolith). This way you'll get many upsides of monorepo without it complexity and at same time most of upsides of microservice architecture without some of it complexity.
The embedded microservices use Uncle Bob's "Clean Architecture", check Example Go microservice for more details.
Table of Contents
api/*
- definitions of own and 3rd-party (in api/ext-*
)
APIs/protocols and related auto-generated codecmd/*
- main application(s)internal/*
- packages shared by embedded microservices, e.g.:
internal/config
- configuration (default values, env) shared by
embedded microservices' subcommands and testsinternal/dom
- domain types shared by microservices (Entities)ms/*
- embedded microservices, with structure:
internal/config
- configuration(s) (default values, env, flags) for
microservice's subcommands and testsinternal/app
- define interfaces ("ports") for The Clean
Architecture (or "Ports and Adapters" architecture) and implements
business-logicinternal/srv/*
- adapters for served APIs/UIinternal/sub
- adapter for incoming eventsinternal/dal
- adapter for data storageinternal/migrations
- DB migrations (in both SQL and Go)internal/svc/*
- adapters for accessing external servicespkg/*
- helper packages, not related to architecture and
business-logic (may be later moved to own modules and/or replaced by
external dependencies), e.g.:
pkg/def/
- project-wide defaults*/old/*
- contains legacy code which shouldn't be modified - this code
is supposed to be extracted from old/
directories (and refactored to
follow Clean Architecture) when it'll need any non-trivial modification
which require testingswagger.json
from gRPC .proto
files.env.sh.dist
to env.sh
.env.sh
and update for your system as needed.alias dc="if test -f env.sh; then source env.sh; fi && docker-compose"
and then run dc
instead of
docker-compose
- this way you won't have to run source env.sh
after
changing it.configs/insecure-dev-pki
, which
was created this way:$ . ./env.sh # Sets $EASYRSA_PKI=configs/insecure-dev-pki.
$ /path/to/easyrsa init-pki
$ echo Dev CA $(go list -m) | /path/to/easyrsa build-ca nopass
$ /path/to/easyrsa --days=3650 "--subject-alt-name=DNS:postgres" build-server-full postgres nopass
$ /path/to/easyrsa --days=3650 "--subject-alt-name=DNS:localhost" build-server-full ms-auth nopass
$ /path/to/easyrsa --days=3650 "--subject-alt-name=IP:127.0.0.1" build-server-full ms-auth-int nopass
To develop this project you'll need only standard tools: go generate
,
go test
, go build
, docker build
. Provided scripts are for
convenience only.
env.sh
in every terminal used to run any project-related
commands (including go test
): source env.sh
.
env.sh.dist
change (e.g. by git pull
) next run of source env.sh
will fail and remind you to manually update env.sh
to match
current env.sh.dist
.go generate ./...
- do not forget to run after making changes related
to auto-generated codego test ./...
- test project (excluding integration tests), fast./scripts/test
- thoroughly test project, slow./scripts/test-ci-circle
- run tests locally like CircleCI will do./scripts/cover
- analyse and show coverage./scripts/build
- build docker image and binaries in bin/
dc
(or docker-compose
) to run and control
the project.
env.sh
.dc up -d --remove-orphans # (re)start all project's services
dc logs -f -t # view logs of all services
dc logs -f SERVICENAME # view logs of some service
dc ps # status of all services
dc restart SERVICENAME
dc exec SERVICENAME COMMAND # run command in given container
dc stop && dc rm -f # stop the project
docker volume rm PROJECT_SERVICENAME # remove some service's data
It's recommended to avoid docker-compose down
- this command will also
remove docker's network for the project, and next dc up -d
will create a
new network… repeat this many enough times and docker will exhaust
available networks, then you'll have to restart docker service or reboot.
$ docker run -i -t --rm ghcr.io/powerman/go-monolith-example:0.2.0 -v
mono version v0.2.0 7562a1e 2020-10-22_03:12:04 go1.15.3
Use of the ./scripts/build
script is optional (it's main feature is
embedding git version into compiled binary), you can use usual
go get|install|build
to get the application instead.
$ ./scripts/build
$ ./bin/mono -h
Example monolith with embedded microservices
Usage:
mono [flags]
mono [command]
Available Commands:
help Help about any command
ms Run given embedded microservice's command
serve Starts embedded microservices
Flags:
-h, --help help for mono
--log.level OneOfString log level [debug|info|warn|err] (default debug)
-v, --version version for mono
Use "mono [command] --help" for more information about a command.
$ ./bin/mono serve -h
Starts embedded microservices
Usage:
mono serve [flags]
Flags:
--example.metrics.port Port port to serve Prometheus metrics (default 17002)
--example.mysql.dbname NotEmptyString MySQL database name (default example)
--example.mysql.pass String MySQL password
--example.mysql.user NotEmptyString MySQL username (default root)
--example.port Port port to serve (default 17001)
-h, --help help for serve
--host NotEmptyString host to serve (default home)
--host-int NotEmptyString internal host to serve (default home)
--mono.port Port port to serve monolith introspection (default 17000)
--mysql.host NotEmptyString host to connect to MySQL (default localhost)
--mysql.port Port port to connect to MySQL (default 33306)
--nats.urls NotEmptyString URLs to connect to NATS (separated by comma) (default nats://localhost:34222)
--stan.cluster_id NotEmptyString STAN cluster ID (default local)
--timeout.shutdown Duration must be less than 10s used by 'docker stop' between SIGTERM and SIGKILL (default 9s)
--timeout.startup Duration must be less than swarm's deploy.update_config.monitor (default 3s)
Global Flags:
--log.level OneOfString log level [debug|info|warn|err] (default debug)
$ ./bin/mono -v
mono version v0.2.0 7562a1e 2020-10-22_03:19:37 go1.15.3
$ ./bin/mono serve
mono: inf main: `started` version v0.2.0 7562a1e 2020-10-22_03:19:37
mono: inf serve: `serve` home:17000 [monolith introspection]
example: inf natsx: `NATS connected` url=nats://localhost:34222
example: inf goose: OK 00001_down_not_supported.sql
example: inf goose: OK 00002_noop.go
example: inf goose: OK 00003_example.sql
example: inf goose: goose: no migrations to run. current version: 3
example: inf natsx: `STAN connected` clusterID=local clientID=example
example: inf serve: `serve` home:17001 [JSON-RPC 2.0]
example: inf serve: `serve` home:17002 [Prometheus metrics]
example: inf jsonrpc2: 192.168.2.1:46344 IncExample: `handled` 1
example: inf jsonrpc2: 192.168.2.1:46352 Example: `handled` 1
example: inf jsonrpc2: 192.168.2.1:46356 Example: `handled` 2
example: ERR jsonrpc2: 192.168.2.1:46364 Example: `failed to handle` err: unauthorized 0
^C
example: inf serve: `shutdown` [JSON-RPC 2.0]
example: inf serve: `shutdown` [Prometheus metrics]
mono: inf serve: `shutdown` [monolith introspection]
mono: inf main: `finished` version v0.2.0 7562a1e 2020-10-22_03:19:37
internal/svc/*
adapters calling some other services.internal/sub
(or maybe use JetStream instead of STAN?).