Mobx-keystone: Road to domain-driven design
Many applications aim to provide users with a dashboard with a soup of features crashed inside. It soon become impossible to manage. This is the time domain-driven design come into play.
Mobx-keystone is a easy-to-use state management library. The state of the application is stored in a mutable tree-like structure. Immutable snapshots are automatically generated each node upon update, as well as being able to give patches as in immer. All these form an alternative to the redux ecosystem, getting serializable actions and action replaying out of the box, and a lot more, such as the ease of data validation.
Mobx-keystone is heavily inspired by mobx-state-tree. Both libraries offer a mutable tree-like structure, and bunch of add-ons and utilities are attached onto the state tree. Compared to MST, which is also based on mobx, mobx-keystone allow classy declarations of models that let engineers to harness the full power of typescript. Users from MST could shift to mobx-keystone without hassles, while the keystone variant in many cases are faster than MST. Nevertheless, the greatest thing is that mobx-keystone is really standing from the viewpoint of a typescript user. It works seamlessly with typescript, which empower larger projects along with countless advantages.
(taken from official documentation)
So what next? How can I build application using it? A todo example is far inadequate for one to make an application further. State management has been a headache for years as we hope to put everything into a single UI . Many applications aim to provide users with a dashboard with a soup of features crashed inside. It soon become impossible to manage. This is the time domain-driven design come into play.
Beyond frontend
What is a frontend? Frontend is frontend. Is that the case?
It might be a little weird to say the term “domain-driven design” in frontend. DDD usually comes with microservices that is traditionally exclusive to backend. Actually, the recent state management in the javascript ecosystem resembles different perspectives in domain-driven design. You may be already using some of it without notice. I wrote this article because I see no noises of discussions and little awareness of being applying DDD in the javascript community. This is the article to bring DDD into your actual development. By grabbing up the important concepts from DDD, this in turn helps you to structure your code better, getting the most out of the library.
Javascript is no longer the prisoner living inside the isolated browser window.
By convention, we usually call the application that lives inside the browser as frontend. This term is clear enough to indicate it is running of the client side, but actually no implications more than that. After some days the concept of frontend got separated from anything else. People use the term “frontend” to represent, ummm, the frontend itself.
What makes frontend unique? Distinct from any other application, Javascript is the “language of browser”. In the past, we use jQuery to manipulate the DOM. Since the past decade, node .js brought javascript out of a browser, plus, more importantly, npm, as well as ES6, the evolution of standard. It is no longer the prisoner living inside the isolated browser window.
The trap
Therefore, just like the famous saying “Objects are a poor man’s closures”, interchangably, the sameconcept does also apply to the javascript community: frontend is actually a stateful backend and a stateful backend is basically frontend. Imagine a game server: It is realtime, with states passing back and forth between the central server and the clients. If there is a GUI on the server, then it is essentially one of the clients with the right of judging the truth of the world in the game. The is exactly what is happening for some P2P games the host client is taking care of everything. That is essentially a distributed system.
Why still stuck on the “frontend” keyword? I hate medium articles that exaggerate things, but it is just totally meaningless to try to convey anything using the word “frontend” and people just falling into a cognition trap by using this word. What we are taught in Science101 is that there is no discrete area of study while I was taking my degree in science. All knowledge are all linked, or overlapped in some ways. The future is interdisciplinary. This translates to the so-called fullstack developer in the industry. They are able to utilize the experience in some aspect of a system to address the issue happening in another aspect of the system. Domain-driven design is not an exception.
Dev technologies are all linked, or overlapped in some ways. The future is interdisciplinary. Domain-driven design is not an exception.
DDD is nothing more than a set of development guideline to adhere to the DRY principle
I am not re-iterating the concepts of DDD here, and I found useful learning from the courses by Vladimir Khorikov. PluralSight is offering free access in April, so don’t miss this opportunity to get much out of it.
Here assumes you are already familiar with basic DDD concepts. It is difficult to memorize everything, but everything is just inside a diagram. Once you are able to point out one of them, recalling the rest is easy. This high cohesion is also a strong signal of the codebase that DDD is correctly in play.
A quick review
Starting with the bounded context, just imagine unwrapping an onion: Inside the boundary, aggregate root is lying inside. Aggregate root is an entity that holds other entity. An entity is a object that carries an identifier. Any entity with the same tag means the same entity so an entity is living no matter the instance get destroyed and created again as long as keeping the same ID. In contrast, a value object cannot live on its own. The lifetime of a value object depends on its parent entity. It embed onto its parent as a sub-document instead of occupying a table. Value objects are immutable so any instance that having the same values in all of the fields could be simply treated as the same object.
A domain model encapsulate some stupid simple logic in order to maintain a valid state. The model is neither data transfer objects nor data access objects. This keeps it independent to infrastructure, such as the method of persistence, or a particular message queue employed in a microservice. As a user you interact with the application service, the outermost of the onion, that’s it. Done.
Role of a framework
If you are still feel frustrated by a sea of keywords, don’t afraid as DDD is nothing more than a set of development guidelines to help you to adhere to the DRY principle. I want to emphasis the word “help” because it is not deliberately creating a bunch of papers to remember. Frameworks also help to to adhere to a set of design principles. Under the learning curve of a framework, there is more of the design philosophy than the surface of API to call over here and there.
If you are familiar with redux we all know that redux is an application of the command pattern and adhere to the reduce functional design, so anything that fulfill inputting a state and returning a new instance of state is a legitimate reducer. One-sentence documentation on the redux official site has explained everything! However I felt uncomfortable so I use mobx-keystone. Well, it is not that redux sucks and it is totally up to your choice! It eventually achieve the same goal but a slightly different set of philosophy. We will go through some typical features in mobx-keystone and relate it to domain-driven design.
Entities
A node in defined in mobx-keystone is an entity. Class models have to be require a unique across-application ID for the class type. The identifier identity is as simple as first checking its type identifier and then its instance identifier:
You might not even need such function defined because mobx-keystone support traversing the tree out of the box by identifier identity.
By default, we need to call model actions in order to be able to change model data, no matter the field is altered inside a method in the model class or elsewhere, unless upon explicit forcing to allow modification (which is highly not recommended). Mobx-keystone, just like immer, provides Map and Array out of the box, so that it is not possible to, say, push an item to the end of the array when the protection is on. These data become mutable in method decorated with @modelAction inside the model class.
Just like reducers as the sole place to alter the state, mobx-keystone offers encapsulation in a more friendly way. Compared to DDD implementation in other languages, it is even more convenient for no need in declaring private fields as to backup the public field. No more getXXX methods! We could give setYYY more meaningful names to make them self-documentary adhering the ubiquitous language. On the downside, all model data is publicly visible in typescript.
An entity has its lifecycle. Mobx-keystone allow us to define life-cycle event hooks. It is good place to initialize volatile states, such as a listener, and dispose it when the node is no longer being included in the state tree. Meanwhile, it is generally not a good practice to have impure entity. Although it is a good place for managing volatile state, you have to control it yourself so that you are not doing things like initializing a repository inside an entity.
Value objects
As mobx-keystone attempted to treat every nodes as entites, value objects are not first-class citizens. Defining value objects in types.object is not useful because we cannot define any methods on a typescript interface. It will become plain objects at runtime. This ends as an anemic domain model.
Although nodes are designed to be mutable through actions, value objects could be immutable by defining no action methods: Any method will return a new instance of the value object.
Another approach is to define mutable class model as usual. Although $modelId will still be generated, we could define an additional method for equality. Having mutable value objects here is not really bad practice as we will go over snapshots, the immutable plain object, in the next article. Nevertheless, defining it in the usual way is just ok.
No matter taking which approach, note that the same node could not appear twice in the state tree. Therefore, if we are defining value object by convenience, we have to make it in the factory manner otherwise we will be reusing the same node.
The factory method in some way prevent having multiple objects pointing to the same node but that node get mutated by somewhere else who is unware of the sharing. If a node is to be shared, we can create a reference. References mean that the node do not own this member but rather it is only holding its id. Based on that we never use id to distinguish value objects, instantiating value object is much cheaper than introducing confusions.
As being said that mobx-keystone offers great freedom, you are once again warned not to put entity in a value object.
A bounded context
In DDD, we all know the rule of thumb is to designate one repository per aggregate root. The outer layer should always be interacting with the aggregate root. Mobx-keystone has the exact same term “root”. By registering the root, it become easier to enforce interacting through the root.
Note that any arbitrary node in mobx-keystone are eligible to become the root store, so watch out. On the hand other hand, as multiple roots are legit, this hint that there could be multiple bounded contexts running in a single process.
More on the root store could be found in the official documentation.
Other component of DDD, such as persistence and domain events, will be discussed in the next article.
It is increasingly common to have a single UI codebase to integrate with a bunch of services. While the backend is employing domain-driven design to split into microservices, the same idea could be applied in state-of-the-art state management libraries. Having DDD into play is hard at first: Lots of jargon, an additional domain layer. In the long run the pain will be gone, and I hope to see it become enjoyable to work with.