Open-source project
MarioCarrion/todo-api-microservice-example avatar
MarioCarrion/todo-api-microservice-example

todo-api-microservice-example: Go microservices you can read end to end

Go microservice tutorial project using Domain Driven Design and Onion Architecture!

1,344 stars181 forksGoMIT

At a glance

What is it?
This repository is an educational Go microservice built around domain-driven design and onion architecture, and it is explicit that it is neither a template nor a framework but a collection of patterns and guidelines for delivering enterprise services in Go. Its most useful feature is the checklist, which shows what the author has implemented and what they have deliberately not.
Who is it for?
Adopt this repository as a reference for how one author structures a Go microservice, and copy the parts you agree with rather than the whole thing, since the README states plainly that it is not a template and that trade-offs were made.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • MarioCarrion/todo-api-microservice-example1,344 ★ · -1
Star history
Repository2026-10-172026-11-07
MarioCarrion/todo-api-microservice-example1.3k1.3k

-1 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A collection of patterns, not a starting kit

The repository introduces itself as an educational one, a microservice written in Go, used as the principal example of a video series called Building Microservices in Go. Then it makes an important clarification. This repository is not a template and not a framework, it is a collection of patterns and guidelines the author has successfully used to deliver enterprise microservices when using Go, and, as with everything in software development, some trade-offs were made.

That sentence decides how to read the code. A template is something you copy and delete from. A collection of patterns is something you read and decide about, and the trade-off is that a pattern only makes sense relative to the judgement that produced it.

The three goals are stated in the introduction, and they are worth quoting because they explain the choices. It is enterprise, meant to last for years. It allows a team to collaborate efficiently with little friction. And it is as idiomatic as possible.

The domain-driven design influence is explicit, with the ideas of Eric Evans from the book Domain Driven Design. The author suggests reading Domain-Driven Design Distilled first, and mentions that a free DDD Reference is also available. That recommendation is a good one for a reader who is going to spend an afternoon in this code, because the vocabulary in the package names assumes the concepts.

The features list is presented as a checklist with boxes, and the boxes are the most useful part of the document. Some are ticked, and some are not.

The project structure, and why cmd/ is plural

The structure section is scoped to microservices specifically, and the README notes that everything in angle brackets depends on the domain being implemented and the bounded context being defined.

At the top there is dockerfiles/, which defines all the Dockerfiles used by the different applications in the project. Then cmd/, which is not a single binary directory but a set of them: a primary server that uses the primary database, a replica server that uses read-only databases, and binaryN for further binaries.

That plural structure is the point of the example. A microservice that wants read scaling has to expose a second entry point that cannot write, and doing that inside one binary means the read-only path is enforced by configuration that can be misconfigured. Separate binaries, each with its own main package, make the distinction impossible to get wrong at runtime, at the cost of a slightly larger build and a little duplicated wiring.

Then there is db/, with migrations/ holding the database migrations and seeds/ holding a file meant to populate basic database values. The seeds directory is unticked in the checklist, which is a small signal about how the author prioritises.

Finally internal/, which defines the core domain. That is the part worth reading closely, and the next section is about it.

Repository, rest and service: the core domain

The internal/ directory is where the architectural opinion lives, and it has three moving parts. A datastoreN directory holds a concrete repository used by the domain, for example postgresql. A rest/ directory defines the HTTP handlers. A service/ directory orchestrates use cases and manages transactions.

The arrangement is the onion: the domain logic does not know it is being stored in PostgreSQL, the HTTP layer does not contain business rules, and the service layer is the only place that knows both. The repository is an interface implemented by a concrete datastore package, so the domain can be tested with a fake and the SQL stays in one directory.

The rule for adding a bounded context is stated explicitly, and it is the scalability test for the whole pattern. When a new bounded context is needed, define a package like internal/<bounded-context> that follows the same structure, with its own <datastoreN>, http and service subpackages.

That is the interesting constraint. A structure that only works for one context is a project layout. A structure you can replicate per context is an architecture, and the README's angle bracket notation exists to make the replication obvious.

The rest/ and service/ naming difference between the top level and the per-context example, rest versus http, is a small inconsistency in the document, and worth noticing if you are copying the layout rather than the idea.

The compose file is the architecture diagram

The docker-compose files at the root show the local environment the example expects, and they are more informative than a services list would be.

The base compose.yml brings up PostgreSQL, Vault and Elasticsearch. PostgreSQL runs the 18.6 bookworm image with a healthcheck that runs pg_isready every ten seconds, and a separate postgres_setup service waits for that healthcheck and then runs the migrations with the tern image, ghcr.io/jackc/tern:v2.4.3, pointing at the migrations directory mounted from the repository:

yaml
  postgres_setup:
    image: ghcr.io/jackc/tern:v2.4.3
    restart: no
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./db/migrations/:/db/
    entrypoint: ["/tern", "migrate", "--migrations", "/db/", "--conn-string", "postgres://user:password@postgres:5432/dbname?sslmode=disable"]

The pattern of a setup container that waits for a healthy dependency and then performs a one-shot job is worth copying, since it removes the race between a database that is starting and a migration that needs it. The Vault service runs in dev mode with a fixed root token, and Elasticsearch has its own healthcheck against the cluster health endpoint, with a second setup container that creates the index mapping.

There are separate compose files for Kafka, RabbitMQ and Redis, plus a common one, so the example brings up only the infrastructure a given test needs rather than everything at once.

The dependency list confirms the same set of tools in Go: pgx and the database/sql standard library, the confluent Kafka client, the RabbitMQ amqp client, go-redis, the Elasticsearch client, a Vault API client, gomemcache, the ozzo-validation library, a circuit breaker, tollbooth for rate limiting, zap for logging, and testcontainers with modules for every one of those services, which is how the integration tests get real dependencies.

The toolchain is part of the architecture

The Makefile is short and it is the clearest statement of what the author considers non-negotiable. The default target is lint and test, and there is a tidy target that runs go mod tidy for both the main module and internal/tools, and a tools target that installs the project's tool binaries.

make
tools:
	go install -C internal/tools \
		github.com/fdaines/spm-go \
		github.com/golangci/golangci-lint/v2/cmd/golangci-lint \
		github.com/google/yamlfmt/cmd/yamlfmt \
		github.com/jackc/tern/v2 \
		github.com/maxbrunsfeld/counterfeiter/v6 \
		github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen \
		github.com/sqlc-dev/sqlc/cmd/sqlc \
		golang.org/x/vuln/cmd/govulncheck

Each entry maps to a decision. spm-go is installed from the separate tools module, and that module is what keeps the tools out of the service's dependency graph. golangci-lint and yamlfmt are the linters. tern is the migration tool, the same one the compose file runs. counterfeiter generates fakes, which is how the repository pattern gets its test doubles. oapi-codegen generates the client and server code from the OpenAPI specification, and the repository holds an openapi/ directory with an oapi-codegen configuration and a generated client.gen.go. sqlc generates the SQL code from the schema. And govulncheck checks the dependency tree for known vulnerabilities.

The last two are the ones most projects skip and most enterprise policies require, so the presence of both in a teaching repository is a statement about the author's baseline.

Where this example is the wrong starting point

The first wrong-tool case is a team that does not want onion architecture. The layering is real cost. Every request crosses the rest, service and repository boundaries, every mock in a test is a faked interface, and a small CRUD service ends up with more packages than behaviour. The README's third goal, as idiomatic as possible, is the author's own resolution of that tension, and it is a resolution, not a proof.

The second is a service that is not enterprise-shaped. The goals say meant to last for years and a team collaborating with little friction, and the structure pays for that with a primary and a replica binary, a Vault integration, a per-context package layout and a heavy toolchain. A prototype or an internal script does not need any of it.

The third is a team that expects completeness. The checklist is explicit about what is not there. Infrastructure as code is unticked, so the local environment is compose files that someone maintains by hand. Redis caching is unticked, and MySQL is unticked, with PostgreSQL the implemented path. The seeds directory is unticked. A reader who copies this as a foundation is copying a partial system, and the author has been honest about which parts those are.

There is also a scope point. The service is a ToDo API, so the domain is small enough that the bounded context story is demonstrated rather than stressed. A real estate domain with a dozen contexts would show whether the pattern holds, and that is not visible here.

MIT, an active main branch, and a checklist with gaps

The project is licensed MIT, with the LICENSE file at the repository root. The repository is not archived, and the last push was on 2026-10-19, the day before this review, so the example is kept in step with the Go ecosystem rather than being a snapshot from when it was written. The go.mod requires Go 1.26, which is a current toolchain, and there are no GitHub releases, which is what you would expect for an application whose purpose is to be read.

The repository layout supports that reading. There is an.envrc for the direnv tool and an env.example, so the configuration story is visible rather than implied. There is a.golangci.yml for the lint configuration, a codecov.yml, and a compose.common.yml alongside the per-service compose files. The docs/ directory holds the written guides, with separate documents for secure configuration, for metrics, traces and logging with OpenTelemetry, and for persistent storage. The internal/tools/ directory is a separate Go module, which is how the tool dependencies are kept out of the main module.

The most useful thing about the project is not the code, it is the checklist with the unticked boxes. A repository that lists what it has not built yet, in the same list as what it has, tells you the author's priorities and the boundaries of their experience, and it does it in a form you can compare against your own project. That is worth more than another layer abstraction.

Editorial conclusion

Adopt this repository as a reference for how one author structures a Go microservice, and copy the parts you agree with rather than the whole thing, since the README states plainly that it is not a template and that trade-offs were made. Do not adopt it as a starting point for a new service if your team does not already work in the onion style, because the layering costs something, and do not expect the unfinished items, infrastructure as code, Redis caching and MySQL, to be there. Before you copy anything, run the compose files to see the local stack work, read the tools documentation under internal/tools, and check the MIT licence at the root.

Frequently asked questions

Is todo-api-microservice-example a template or framework?

No. The README states it is not a template and not a framework, but a collection of patterns and guidelines the author has used to deliver enterprise microservices in Go, with some trade-offs made. Its three goals are that it is enterprise and meant to last for years, that it lets a team collaborate with little friction, and that it is as idiomatic as possible.

How is the project structured?

dockerfiles/ holds the Dockerfiles for the different applications, cmd/ holds the binaries including a primary server that uses the primary database and a replica server that uses read-only ones, db/ holds migrations and seeds, and internal/ defines the core domain with a concrete repository per datastore, rest/ for HTTP handlers and service/ for use cases and transactions. A new bounded context gets its own package following the same shape.

What does the local environment look like?

The compose files bring up PostgreSQL, Vault and Elasticsearch, with separate files for Kafka, RabbitMQ and Redis and a common one. The compose.yml includes a postgres_setup service that waits for the database healthcheck and then runs the tern migrations, and an elasticsearch_setup service that creates the index mapping.

Which code generation tools does the project use?

The tools target installs spm-go, golangci-lint, yamlfmt, tern, counterfeiter, oapi-codegen, sqlc and govulncheck, all managed as a separate module under internal/tools. The repository holds an openapi/ directory with an oapi-codegen configuration and a generated client.gen.go.

What is not implemented in todo-api-microservice-example?

The features checklist leaves infrastructure as code, Redis caching, MySQL and the database seeds unticked. PostgreSQL is the implemented storage path, with Memcached for caching, and the project is MIT licensed with the last push on 2026-10-19.

Official sources

  1. Issues
  2. License: MIT
  3. MarioCarrion/todo-api-microservice-example on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mariocarrion-todo-api-microservice-example.svg)](https://hysenlabs.com/projects/mariocarrion-todo-api-microservice-example)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/mariocarrion-todo-api-microservice-example.svg)](https://hysenlabs.com/projects/mariocarrion-todo-api-microservice-example)

Was this useful?