Previous post in HappyPancake story: Reactive Prototype.
I started merging bits of my reactive prototype into the document-driven prototype of HappyPancake that Pieter was working on. While at that, we spent a lot of time discussing the design and iterating over it.
It was really cool to see how the structure of the solution shifted focus from technical model to functional model. Previously our golang packages (which roughly map to lightweight .NET projects) contained files grouped by their technical intent (e.g.: controllers, models, documents). This added friction to development:
- a lot of context switching was required in order to work on a single use case, touching multiple packages;
- solution structure enforced certain architecture style upon the codebase (when you have folders like models, controllers, views and documents, naturally you will be trying to fit your implementation into these);
- merge conflicts were unavoidable, since too much code was shared.
Over the course of the week, we switched to a different design, aligning packages with use cases. You might consider this to be a tactical domain-driven design (we didn’t touch any of the strategic parts like Bounded Contexts or Ubiquitous language, since our core domain is extremely simple).
Golang packages get tightly aligned with our use cases. They either implement cases directly (e.g.: by exposing http handlers to render the UI and process POST requests from the browser) or they help other packages to fulfill their role by providing supporting functionality or structures (e.g. authentication utils, http helper methods, core value objects).
Of course, the road wasn’t all about roses and pretty ladies - you can’t just split codebase between a bunch of folders and hope that all will work and make sense. It is never that easy.
We had a lot of discussions like :
- How do we decompose functionality into multiple packages which will work together to implement these use cases?
- This code does not make any sense, what are we doing wrong?
- How do we name this thingy?
- What is the simplest approach to implement these use cases?
- How can we work together on this functionality?
I really enjoyed every minute of these discussions with Pieter, they were focused on the problem domain instead of fiddling around artificial architectural constraints imposed by the overall design. Besides, so far, we were able to resolve these questions and thread the thin line between over-engineered monolith and messy big ball of mud.
We are not sure if we’ll be able to walk this path later, yet so far each step led to a deeper insight in the domain of HappyPancake (just like domain-driven design promises). There are a few really cool things about our current design:
- it is extremely easy to collaborate on the code : there are almost no merge conflicts;
- we are free to experiment with implementation styles within packages without turning solution into a mess;
- golang is designed to support small and focused packages, this shows up frequently as yet another tiny and deeply satisfying moment.
The most important part is : our code is a reflection of domain knowledge captured in a tangible form. Codebase is structured around that knowledge and not vice versa.
In the meanwhile Tomas was busy with administrative work and HPC1. Towards the end of the week he also got a chance to start working on the HTML design of HPC2 in a stealth mode. Pieter and me are both really anxious to see what comes out of this work.
Also on Friday we were interviewed by a couple of students on the topic of CQRS. I’d think that our joint statement was something like “CQRS is new name for denormalization with a little recollection of what happened before 2010”.
Next post in HappyPancake story: Emergent Design Faces Reality