A growing example of a real-world Golang web application, using the best of type-driven design and Hexagonal Architecture.
This codebase showcases advanced techniques for building production-ready applications that are used daily at leading tech firms, but are hard to find examples of in the wild.
These patterns will help you slash production bugs, massively increase your test surface, and painlessly evolve your services as your product grows. Your code will become more readable, more maintainable, and more fun to work on.
You'll find this repo valuable if:
- you're excited about Hexagonal Architecture from what you've learned from Uber and Netflix, but aren't sure how to apply it to your own projects;
- you're inspired by the promise of type-driven design, which makes invalid data extremely hard to represent;
- you're not ready for the cost and complexity of microservices, but you want an architecture that will seamlessly decompose to microservices as you scale;
- you're tired of fixing production bugs, but your current architecture is too hard to test thoroughly;
- you'd like a playground to experiment with microservices patterns, without having to spin up multiple services;
It's not easy to find examples of production-grade web applications in the Go ecosystem. It's even harder to find comprehensive implementations of patterns like Hexagonal Architecture. It's even worse if you're passionate about using the Go type system to its fullest, catching many common bugs at compile time. Finding an example of all three in one place? Near impossible.
For years, I've helped industry-leading companies like Qonto develop the architecture standards used by hundreds of Go engineers. I've introduced or promoted the same techniques at companies as diverse as ISPs and tier-one investment banks. I've spoken publicly about the benefits and implementation of type-driven design in Go.
I'd have killed for high-quality examples like this when I started that journey.
In this repo, I've combined everything I've learned about building bomb-proof web applications in Go. I hope you find these patterns as valuable as I have. If you have questions or feedback, open an issue or drop me a line at github@angus-morrison.com.
This app implements the RealWorld specification for a Medium-like blogging platform. The RealWorld spec reflects a small but realistic subset of the features you'd find in a genuine product.
This implementation uses Hexagonal Architecture to separate business domains from each other, and from the infrastructure that ferries data in and out of the application. The example uses JSON over HTTP as its transport layer and SQLite as its datastore, but our business logic doesn't care. If we moved to gRPC and MongoDB, nothing in the business logic would need to change. This is an immensely powerful tool for any development team.
But this app goes further by using type-driven design to enforce the validity of data passed into our domains. If a domain object exists, it's valid. It takes real effort to write code that hands the domain bad data, which means entire families of sloppy mistakes are eliminated at compile time.
Hexagonal Architecture. You might have heard about it under the name "Ports and Adapters" too.
It was first proposed by Alistair Cockburn to address the symmetrical problems of business logic becoming tangled with the UI layer, and the tight coupling of an application to its database.
Netflix have written about it. Uber's Go code structure will also look familiar to Hexagonal Architecture enthusiasts.
In essence, your business (or "domain") logic – the stuff that makes your product unique – lives at the heart of your service. It exposes interfaces ("ports") that infrastructural concerns like databases and web servers must conform to (hence the term "adapters"). Your business logic doesn't care about the nature of the infrastructure it's plugged into, allowing you to swap out DBs without touching the code that really matters. You can use these same ports to mock dependencies during testing, granting exceptional coverage using only lightweight unit tests.
Impressively, this pattern has survived the transition from large, enterprise monoliths to modern microservice architectures. It's that ability to adapt to changing requirements that makes Hexagonal Architecture so powerful.
According to Alistair:
The hexagon is intended to visually highlight
(a) the inside-outside asymmetry and the similar nature of ports, to get away from the one-dimensional layered picture and all that evokes, and
(b) the presence of a defined number of different ports – two, three, or four (four is most I have encountered to date).
Any nested shape will do. Don't overthink it.
Type-driven design. The secret sauce that elevates Hexagonal Architecture to Michelin-starred bliss.
Perhaps you've heard the phrase, "Parse, Don't Validate"? It expresses the ideal that, rather than rely on runtime validation to ensure the correctness of our data, we can instead use the type system to guarantee that our data is valid. If it compiles, it's valid.
Wow. Take a moment to let the power of that concept sink in.
Now come back to reality, because the "Parse, Don't Validate" blog post was written about Haskell, and Go's type system is... well, it's not Haskell's, put it that way.
However, the Go type system has just enough juice that we can make it hard to pass our business logic bad data. Users can't do it, and developers would have to do it deliberately. Or have a bad accident.
By ensuring that domain endpoints accept only domain models, and by defining constructors for those models such that
bad inputs are always rejected, we can be confident that our business logic is always working with valid data. If
you hold an object of type Username
, you know it's a valid username. End of.
Not only does this reduce the number of nasty surprises you get in production, it extracts all the noisy validation code out of your business workflows, and encapsulates it neatly in constructors. This has the triple benefit of making the validations easy to test, making the business logic easier to read, and reducing the number of ways your business logic can fail... which makes it easier to test.
If you'd like to learn more about type-driven design in Go, including what happens when you take it to its comical extreme, check out my talk on the subject from London Gophers.
The project root. Contains go.mod
, linter and generator config, Dockerfile
and the root Makefile
.
Static assets used by this README. Not part of the application.
Compiled binaries.
The application's entrypoints. Contains the main
package for each binary, which is responsible for bootstrapping the
application.
The entrypoint for the HTTP server. Currently the only entrypoint.
The default host directory used to mount file dependencies into the Docker container during local development. Contains assets that shouldn't be committed, such as the RSA key used to generate JWTs, and the development database.
The entire contents of ./data
are gitignored, so you won't see it in this repo. To generate all the dependencies
you need for local development, run make generate
.
Library code specific to the application.
Loads application configuration from the environment.
Contains the business logic of the application, with one domain package per business domain. E.g. ./domain/user
, for
all business logic concerning users.
Inbound adapters. These are responsible for:
- Translating inbound requests from specific transport technologies into transport-agnostic domain requests.
- Invoking a domain
Service
instance with the domain request. - Translating domain responses back into transport-specific responses.
The REST API adapter that satisfies the RealWorld spec. This spec isn't technically RESTful, but "jsonoverhttp" makes for a much worse package name.
Outbound adapters. These are responsible for:
- Implementing outbound ports defined by the domain.
- Translating requests from the domain into transport or data storage requests.
- Translating responses from the transport or data storage layer into responses accepted by the domain.
This app uses SQLite as its datastore. This package provides the SQLite
type, which satisfies the user.Respository
domain port.
A collection of test utilities useful to all packages.
Library code that could be used by other applications, but that I haven't got round to extracting into a separate repo yet.
Build scripts and entrypoints.
Makefiles for each major task family, which are imported by the root Makefile
. This makes managing a large collection
of tasks much easier.
Here's what's been implemented so far.
- Users
- Authentication
- Profiles
- Articles
- Comments
- Tags
- CI pipeline
- Optimized Docker image
- First-class error handling
- Linting
- Extensive, concurrent unit test suite
- Health checks
- Streamlined local development experience
- Optimistic concurrency control
- Concurrent integration tests
- Structured logging
- Metrics
- Tracing