This is my response to an email asking about Domain-Driven Design in golang project.
Thank you for getting in touch. Below you will find my thoughts on how golang works with DDD, changing it. This is merely a perception of how things worked out for us in a single project.
That project has a relatively well-known domain. My colleagues on this project are very knowledgeable, thoughtful and invested in quality design. The story spelled out below is a result of countless hours spent discussing and refining the approach.
Conclusions could be very different, if there was a different project, team or a story-teller.
golang is a very simple and boring language. Unnecessary complexity doesn't feel right there. Its mindset and ecosystem help to prioritize approaches and concepts over their relative important to the design process. Hence we focused only on the things from DDD that worked best for us.
Domain events, domain modules and event-driven use-cases worked well for us in golang, along with iterative prototyping, forms of event storming.
We found these to be an implementation detail that is less relevant to the overall design: immutability, aggregates, concept of entities and identities, aggregate roots and aggregates, event sourcing (as defined in .NET community).
It is nice to hear that you had similar thoughts about DDD and overdoing things in golang like we did. I believe, currently in .NET and Java spaces we tend to build relatively complex architectures in the code, simply because that's how ecosystems work. That is what people are used to and expect. We enforce design there by introducing abstractions and even building frameworks.
Golang is different. It is still strongly typed, but has a more functional feel. The entire ecosystem relies more on conventions and light-weight interfaces. Applying concepts from .NET/Java directly would feel like over-engineering.
That's why we just ignored a lot of concepts one would find out in .NET/Java project. Here's a quick summary of how we reasoned about backend:
1. Domain Events
The most important things are domain events. They are aligned with the business domain and it's language. One can use event storming and iterative prototyping (from different perspectives) to capture and refine them. Once captured, they rarely change. We express behaviors in events. The system is tested by verifying use cases, which are expressed in events. Modules communicate via events.
Events are a reflection of
Ubiquitous language, acting as the link between the business language and the code.
Alberto Brandolini and Gregory Young are very vocal about events in Domain-Driven Design. If you haven't done so already, I recommend reading everything they have to say about that. Vaughn Vernon also dedicated portions of his book to that topic.
2. Domain Modules
The next important thing - domain modules. Modules help to structure our domain, decomposing it down into small focused elements. They are designed to work together to provide the necessary functionality.
Concept of "micro-services" has a lot in common with this approach, just like the foundational principles from Object-oriented programming. I highly recommend watching inspirational talks by Fred George.
In our solution, each module is represented as package. Modules implement behaviors, grouping related functionality in the same package and hiding dirty implementation details behind the nice contract. Contract is expressed in interface:
- Module can subscribe to events (user approved, photo liked, payment received etc)
- Module can expose HTTP REST endpoint (approve photo, register, get account details etc)
- Module can publish events
Modules have distinct data storages. They interact only through the interface (and can be deployed on different machines).
We don't have any notion of aggregates, aggregate roots, repositories or process managers in the design, they weren't needed. We might have these concepts inside the module, but that would be an irrelevant implementation detail. Event sourcing is aligned with the definition of Martin Fowler (not the way it is usually done in .NET/Java communities).
Modules are relatively small, golang allows that. They merely group related behaviors together. In that sense their design process is similar to how one would group similar behavior and state in aggregates. A bounded context can be implemented as a dozen of small modules, if a language permits that easily. Golang does that.
Modules are constrained to be a separate unit of deployment and scaling (we can deploy them to different machines in multiple instances).
To be more precise, domain design, expressed in modules, their contracts and relations is important. Implementation details are less relevant, even the language in which they are written. If you have a decent domain design, throwing out the code and rewriting modules one by one is trivial.
We ended up with a dozen of small and focused modules, each represented as a separate golang package.
Although, not much emphasis was placed on the implementation details, there were a few guidelines:
- We denormalize heavily in the modules. Module could subscribe to the needed events and maintain a local view model, used for decision making, enriching HTTP responses or published events. It works ok for systems with millions of events and above.
- Modules are idempotent. This is verified by running tests, derived from use-cases.
- Modules tend to be small - a few tables, a few queries, a few HTTP routes.
- Performance in this project is so important that it became a part of the domain (we have to serve largest free dating web site in Sweden). This added additional design constraints to the process, making it more technical.
Naming the module was probably the hardest thing (especially, since package names in golang tend to be short). Time spent was well worth it.
Use-cases are the third important thing. They describe and capture individual module behaviors, expressed using the module contract.
- Given some past events
- When we call HTTP REST API endpoint with some params
- Expect HTTP REST response and some optional events.
Use-cases could be used to:
- Verify behaviors of the system
- Generate human-readable documentation, including dependency graphs.
- Auto-generate idempotency, concurrency and stress test suites.
Use cases are our framework. They help to express and verify the needed behaviors. For example, in .NET/Java you could rely on code to enforce immutability, equality checks or valid state. This approach is limiting, since it introduces unnecessary design constraints that don't work well in a language like golang. Such things are an implementation detail of individual modules.
In fact, a module could be thrown away and rewritten in a different language, if needed. As long as the use-cases still work, language is an implementation detail.
If module behaves like it is expected, then it is correct. If there is an error in the code, that is not captured by the use-case, then use-cases are lacking.
Approach of event-driven use-cases stems directly from testing in event-sourcing as taught by Greg Young in his classes.
4. Iterative Design Process
It is very hard to find the right solution from the very start. We iterated many times, trying various frameworks, technologies and design approaches. Current state of the project is merely a combination of things that worked together better than the other alternatives.
Design emerged through countless iterations over multiple dimensions. For example (current choices are in bold):
- Design approaches. We tried applying various bits of DDDesign. Some worked better than the others.
- Development processes. We started from Programmer Anarchy and, as project matured, introduced new processes.
- Databases. We went through MS SQL, mySQL, PostgreSQL, CouchDB, FoundationDB.
- IDEs. We tried Sublime, IntelliJ IDEA, Atom, Brackets, Vim, Emacs.
- Messaging approaches. We evaluated: ZeroMQ, NanoMSG, FoundationDB high contention queues, CouchDB change feeds, RabbitMQ, NSQ.
The heart of the project is a set of design choices and domain knowledge that we discovered and refined. They are project-specific. Code is merely a by-product that evolved to capture this knowledge. It will evolve further.
5. The Right Team
Having the right team to work in, is the last but not the least important thing. This design approach would work only if the team believes in the importance of the domain design and is willing to continuously invest effort to get it right. I'm lucky to work with Tomas Roos and Pieter Joost on this project.
I hope this helps.
Please also keep in mind: this story is only about a backend. Front-end is going to see a different set of design approaches and principles applied. That is a completely different Bounded Context with its own concepts, language and priorities.
In our case front-end is currently an isomorphic single-page web application with responsive design. It uses uni-directional data flow ideas from Facebook Flux and is based on isomorphic Flux components open-sourced by Yahoo.
If you want to read more about this project, check out the full project log, which I update on a regular basis. Beware, that this is a very long read with many technical details.