How We Structure TANIA Backend
In the product development team of Tanibox, we use the DDD approach. DDD or Domain-driven Design is an approach where we seek to deliver software around our business capabilities.
I came from traditional MVC architecture who built software from designing the database first. But in domain-first architectures, I’ve forced to think the business capabilities first without thinking the database or protocol at all. It struck my mind that has been planted in the past 4-year building software from a database. After I finally grasp the domain-first mindset, I cannot back again to my old way.
The overall architecture of Tania is built so it doesn’t rely on particular storage, protocol, or serialization format. Because it needs to be deployed to the different environment like Raspberry Pi, NAS QNAP, or the cloud like AWS. We name the Tania architecture as Modular Monolith as each domain share nothing.
Before we go deeper into the explanations, reading Tania codebase may help you. Check Tania Core on Github
Tania consists of 3 layers: The Domain, Repository/Query, and Application.
Take a look at the illustration below.
The inside layer cannot access the outside layer, but the outside layer can access all of their layer below. So Domain layer cannot access Repository or Application, but Application layer can access Domain or Repository.
Herein lies the most important piece of our software, as it reflects directly on our business requirements. It consists of an entity, value objects, and their behaviour. In this layer, we only care about what our application can do in the form of entity, value object, or behaviour. We do not care about how to persists to DB or how to maps them to endpoints.
We usually start to think from “What behaviours are this entity should have?” when designing the Domain. The characteristic of behaviour is that they should mutate the state of its entity. For example Farm, Area, and Crop are entities. Farm has ChangeName() and ChangeType() behaviours. Crop has MoveToArea() or Harvest() behaviours.
One more thing about the Domain layer is we can independently test our domain with minimal mocking involved.
And actually, what we’ve done in the Domain layer is basically lighter DDD. We “borrow” some important term from DDD and combine it with our own implementation, that is more suitable for us.
Initially, we start just with an in-memory database to persist data temporarily. But we changed that later with SQLite, then MySQL. This is because, at the beginning of the development phase, we just need to get faster feedback and avoid the troubles that come from persistence engine.
Repository only contains write methods (`Save()`) and Query only contains read methods (`FindByID()`, `FindAll()`). What we will do in the write methods is to save the Domain Entity as a whole, then we get that Entity again as a whole too. When we have more real persistence engine (SQLite, MySQL), we start to create a mapping of table columns as needed.
This is the area where the HTTP Server goes. For this, we implement the Echo framework.
Each domain will have each server that has grouped routes, all mounted in the `main.go`. We can easily add more protocol to communicate with a different client without changing our Repository/Query or Domain layer. Like if we want to be able to run on different protocols (gRPC) or if we want to add a different port for admin’s side with their own authorization.
We initialize our dependencies manually here.
There are handlers too that is mapped by its routing. Basically, this is where we compose our business logic with the other infrastructure, like persistence, event bus, or another external service like email or authorization.
The Domain behaviour is designed with Command and Event in mind. ChangeName() or Harvest() are called Command that will trigger Events NameChanged and CropHarvested respectively. The triggered events can be caught by other services, system, or domain via event bus in the Application layer. Usually something like a Pub/Sub mechanism. It makes our system loosely coupled.
With this architectures, I feel that there is sometimes more work to do when changes happened, especially when it comes from the deepest layer: the Domain. But most of the time, when the big changes happened in persistence or in the protocol, it is very easy to adapt.
And recently we have our new apps named GRO Planter released. GRO utilise Tania OSS as its backend. And there are many improved and lesson learned from our initial version in Tania OSS. I will write about it in another time!
Don’t forget to check Tania source code on Github, and we’ll be very pleased if you want to contribute to the codes too!