Monoid makes design simple.
In my opinion, a software design is successful only when it not only solves the problem at hand but also remains pretty easy to adopt and maintain. Thus, simplicity becomes the key design principle for a software design which needs to be reusable as well as maintainable.
Keep it simple, stupid.
Famously known as the KISS principle, can be seen in action in one of the prominent functional programming pattern: Monoid. Though quite famous in the FP world, it is not a very well known pattern in the object oriented programming as compared to more ubiquitous patterns like Functors and Monads, which are mostly seen as map and flatMap operations on different containers. On the other hand operations like fold and reduce implemented on the collections which put their elements into category of Monoids (Semigroup, to be precise) remain unnoticed by most of the OO developers.
This blog tries to explain how Monoids can help OO developers to simplify their design. However, the prime focus of this blog is not to explain Monoid as a concept in the category theory, but to discuss the design style where a large complex problem is divided into smaller problems and the final solution is achieved by combining the solution to each of those.
The problem: How to construct very large JSON requests ?
We came across this problem while integrating with a system used for managing life cycle of various products offered by our client. Thus, in order to create a product we had to invoke a service endpoint with a JSON request containing necessary details. Since, the same endpoint was used for creating all of the products configured by our client, request fields would change based on product type. Moreover, each request would contain a set of fixed values and many of these values would be dependent on environment.
Let’s see an example request JSON. For brevity, some fields are omitted while the actual request may contain upto 200 fields.
{
"productType": 1, //1 for product A, 2 for product B
"customerDetails": {
"custType": 100, //Always 100
"name": "Bruce Wayne", //Name provided in personal details
...
},
offerCode: 699, //In production value is 394
pr1: { ... }, //Specific to product A, null otherwise
pr2: { ... } //Specific to product B, null otherwise
}
Initial solutions that worked well but could not scale.
The very first approach that worked quite effectively for the first product integration was to define a POJO for the request model. All we had to do was to construct the object, pass it to Spring’s RestTemplate and it will take care of JSON serialisation.
But, as new products came in, the situation became difficult. For each new product we had to define separate constructors with specific defaults. Also, the code was littered with Nullable values as some parts of the object were not applicable to specific products. Most importantly, this meant that the request model would need a change whenever any of the products change. Such a pattern is identified as a code smell named Divergent change.
Though this problem can be solved using inheritance and separating out the construction to a factory. But, the major problem was the dynamic fields requiring runtime override configuration and that was making factory implementation very complex to understand.
For a deeply nested object, managing environment specific overrides and defaults in configuration was quite difficult and the factory would need to maintain this mapping for each field. Also, as more and more fields become dynamic the factory code would undergo a change.
These complications made us think again about the above design and we concluded that the very large request model is the root of all problems. Thus, we decided to get rid of it by taking a completely new approach: Templates.
With templates, the factory becomes surprisingly simple.
Apart from the factory, all we need to do is to define a template like one below.
Problem solved, right ?
I think you have already guessed the answer. We have just shifted our problem from one component to another: the complexity of the factory is now in the template. In addition to that we have also created some new problems.
- With the request model we have also removed the explicit contract. Now just by looking at the template it is quite hard to guess what the type of the field is. Well, this means that the type safety provided by a carefully defined explicit request model is gone.
- The factory is now simple but understanding the template code is hard as it relies on some dynamic intermediate models constructed from input and config.
- Lastly, the template in itself is a new programming language to learn and that is an additional complexity to deal with.
So, now what? Before we jump into another solutioning phase let’s have a look at our learnings from the previous two designs.
- Request models make contracts explicit and provide type safety.
- Very large request models are not really a problem. But,
- Constructing very large request models is a problem.
- Inserting product specific defaults in a very large request model is a problem.
- Overriding request object fields with environment specific configuration is a problem - Intermediate dynamic models give rise to incidental data structures and are very difficult to understand.
So, let’s get back to the design board.
Design considerations
Unlike our previous two implementations we wanted our next implementation to have a better developer experience, that is it should be intuitive, easy to understand and simple to use. Along with it, we wanted the new design to support the following key requirements.
- It should provide type safety / explicit contracts.
- It should simplify construction of very large request objects.
- It should provide the ability to configure defaults.
- It should provide the ability to override fields.
- It should provide the ability to map domain objects to request fields.
Design concepts
A composable model
Now, we already know that constructing a very large request object is a difficult task due to its size. But, if we can divide it into smaller chunks then constructing those is relatively easy.
Divide and conquer.
So imagine that, we have an ability to build various parts of the request object in an independent manner which simplifies their construction. Well, this solves a part of the main problem but, it also creates another problem: How to combine them back into the final one?
Once again, let’s divide our problem and get to the smallest piece of it that is combining two objects to get one. If we could solve it then the same solution can be repeatedly used to combine the next part with the accumulated result. See, the figure below which describes a binary operation that can merge 2 JSON models into one.
Alright, but what will happen when the same keys are present in both parts. Well, that is upto us to define and we say that keys on right will take the precedence. You got it, this gives us ability to override fields. See the figure below describing the overriding using the same binary operation.
Thus, the composable model simplifies the construction of the large request object. But, what about type safety? How would we know if we have constructed a correct model through composition?
A complete model
As compared to the earlier problem this one is relatively simple. To provide type safety, we can associate the JSON model with its corresponding type and the model can be deemed complete only when it’s underlying JSON can be converted into an object of the given type.
Let’s consider a simple class named Person
given below.
Figure below shows different JSON models and also describes whether they are complete or not.
Serialisation to json would fail when the JSON model is not complete and that’s how you get the type safety.
A mapping DSL
Above two design concepts satisfy all the requirements of the JSON model except the ability to easily map domain objects to the request model fields. We could use templates for this job but, we have already discussed the disadvantages of this. Then, what is the alternative to templates? Well the answer is simple: DSL. A DSL implemented in Kotlin gives you functionality of a templating engine with power of Kotlin. Also, it is quite simple to understand due to its declarative syntax.
Now, let’s implement these concepts to get a solution to all the problems stated above.
Implementation: JsonModel<T>
Finally, after going through so much theory it is time to see some actual code. Concepts explained above are implemented as a classJsonModel<T>
. Snippet below describes just the interface of the class while the complete implementation can be found here.
Now, let’s see how we can use this container to build our request objects.
Construction
A JsonModel<T>
can be constructed from various sources like files, JSON text, and from an object of the type it represents. In addition to this, one can also construct an empty model. Code snippets below show a few examples while the complete code can be found here.
Composition
Code snippet below shows an example where a part of the model is loaded from json text and remaining is injected using another object of type Name
.
Overriding
Code snippet below shows an example where a complete model is constructed from a value and one of it’s fields is overridden using the same composition operation.
Mapping
Code snippet below shows an example where a domain object Customer
is mapped to an object of type Person
.
Serialisation to JSON
A JsonModel<T>
can be directly serialised to a JSON using Jackson ObjectMapper
. However, the serialisation will fail if the model is incomplete. See the example below where a JsonModel<T>
of a class Person
is serialised to JSON directly.
Now, let’s see how our RequestFactory will look like when we implement it using JsonModel<T>.
Putting it all together
Alright, to implement the RequestFactory using JsonModel, first let’s identify the various parts for it. Code snippet below shows the class Request with comments on its field indicating the source. Also, it includes the corresponding domain classes.
Finally, let’s have a look at our RequestFactory and related configuration.
Let’s go through each of the files.
The first file is a plain and simple JSON containing the default configuration. Defining this file is quite easy by just copying the entire JSON request but keeping only fields which get their defaults from this file. By doing so we are creating only a part of the request having the exact same structure as that of the original. Hence, making it very intuitive and easy to maintain.
The RequestFactory class upon construction loads the JSON configuration and builds a partial request model. In addition to that, it also maps domain objects to their corresponding parts of the request using the mapping DSL. In the end it simply combines all parts together to get the final Request
object. Use of the DSL once again makes things very intuitive due to the similarity in the structure as well as the declarative syntax.
In the nutshell, our final design provides clarity by using explicit model definition, with declarative syntax in mapping and configuration, and by taking away the responsibility of the object construction from the factory. It is very easy to understand as it divides the problem into smaller chunks and relies on embarrassingly simple concepts like object composition to get the final result.
Now, you might be wondering how all of this relates to a monoid?
I see a monoid in the JsonModel
Yes, from the point of view of algebraic structures, a JsonModel forms a monoid. So, what is the monoid ?
A monoid is a set along with an associative binary operation and an identity element.
The function JsonModel.with
is an associative binary operation and function JsonModel.of
gives you the ability to construct an empty JSON model which can be used as an identity element with JsonModel.with
. Refer to this test which verifies the conformity of the JsonModel to a monoid.
Due to its monoidal nature the JsonModel allows you to use divide and conquer strategy as we split the complex problem of building the large request into smaller and simple parts and in the end we just fuse all of these parts together into the large request.
In essence, monoids help you to control the complexity in your design.
Final Thoughts
Patterns like monoid can help you to make the overall design simple.
Simplicity is a key design principle that makes the final design easier to understand and adopt. Hence, try to make your design as intuitive as possible and always give highest priority to the overall developer experience. However, while embracing the simplicity do not compromise on the functionality of the design.
The code for the entire library can be found in the GitHub repository fusionkt.