Recently my team and I are working on a modular monolith. We’ve been inspired by Kamil Grzybek’s solution available right here. This repository is awesome, and if you haven’t seen it, I strongly recommend taking a look.
Generally, during development, keeping our boundaries when going through the Command/Write part of our CQRS architecture was quite easy. However, once we started thinking about the Query/Read part, we noticed that we could use some data from module A in module B. Then we thought about different approaches how we could get the data efficiently, but also without crossing logical boundaries, which is quite easy, because of no physical separation.
Querying other modules’ data… meh
As the data is stored in one database, it’s tempting to just query data from another schema. However, it creates coupling on the database level. We have set a rule, that you shouldn’t build a query that touches more than one schema. So we decide to not go with this approach. So even though we have only logical separation right now, if we’d decide to physically separate the database as well, it wouldn’t be possible with such coupling. Simple, but it wasn’t acceptable in our case.
Separate Backend For Frontend
Another option was preparing Backend for Frontend approach. The BFF would be responsible for gathering data from different modules and then returning such data to the frontend. The data could be gathered from REST APIs in the backend that hosts the frontend and then converted into a format that is easily useable in the Frontend.
This approach is very reasonable, but I don’t like the fact that within one system I’d have to introduce such overhead. Also, if the modular monolith will need to separate a Microservice outside of it, and the Microservice goes down, it’d be impossible to let the user work with the UI. Even if this is just a different part of the system, that doesn’t need anything else besides one piece of information. But if the part of the system goes down… you ain’t gonna get part of the data.
On the other hand, if the entire modular monolith goes down… well, the same story, isn’t it? But still, as we’re owning the frontend we should actually make it easy to work with, right? After all, I feel like I can do more (and quicker) in the backend. So this is definitely the way to go, but perhaps there’s something better.
Frontend calls REST APIs and connects the data
This approach would be similar to the previous one. Frontend would call multiple REST API endpoints and put it all together into a view. I don’t like this approach because it feels like doing too much on the frontend side. Again, I prefer to prepare the data on the backend and return a view model that can be easily rendered. But that’s my personal preference, based on the fact that I prefer working on the backend. This approach is definitely good for developers that feel more confident working in JavaScript. Personally, I don’t like to push too much pressure there. So I didn’t decide to go with this approach.
Duplicating necessary data to build Views/Read models
As I said, I prefer to work on the backend side and use the frontend just for rendering the data. Additionally, we are currently using CQRS. So our decision was to build eventually consistent Read Models.
But… how to get data?
Okay, it felt weird for me at first glance. But… we just duplicate that between modules. So if module A is the technical authority of some data, and module B needs it, we just get it there through integration events. So if module B is interested in some data from module A, it just subscribes to it’s events. This way, there’s no coupling between those modules. The duplicated, cached data is only used for Read capabilities. This data is never modified outside of module A, because module A is a single source of truth.
Because of that, we’re able to cache the data in the module’s B database and build Views (the database Views) to be able to provide the data that the frontend needs quickly, through raw SQL queries called from Read side of the application.
Also, this way, if this is ever a separate service or for some reason, part of the app is down, we are still able to get the data from the calling module.
Generally, when I think about it, this approach could be also applied to Microservices/SOA as well. It’s not solely for the modular monolith itself.
What do you think about the approach with duplicating the data vs BFF? Which one would you go with?
Top comments (2)
This is great but I feel like it didn't answer half of the equation. How to "GET" data initially between modules. In microservice you might just fire a GET request to another REST service. But how for modular monolith that communicates through the EventBus?
For example Order and Products Catalog are separate modules. When an order is placed the order can store the product data within its schema. This would be all the product data necessary to inform the order as apposed to a Foreign Key reference that would needed to be JOIN( or $lookup) at read time. (I should mention I'm using NoSql so de-normalized data is welcome here) Perfect, clean, great. Orders can be separated later to a microservice, no problem. If product updates then Orders module can listen for changes and update its copy(s) accordingly. BUT when the order is being created how does one cleanly retrieve the data from products catalog in the first place.
Must you trust the User and the client consumer to have retrieved the proper product data and submit correctly with order data? What if there is validation or business logic shared between order and product? Does this have to happen consumer side? I can't conceive of a way without in memory call to GET the product and then be able to work with it in the order domain for CREATE. Any ideas or advise here?
Hey!
In microservices, you mostly have two options for grabbing data from one microservice that owns data to the consumer microservice:
In modular monoliths, you have these equivalents:
use anything that makes the link between the domains. This will always end up being a centralized place (as bus is in the async communication).
In my case, I use adapters and just map types from one service to another service. You can do any operation with this, both reads and writes. This will give you the decoupling between services. The only coupling happens in this adapter file/package.
I place this next to my HTTP handlers because I consider it transportation of data, it is not HTTP handlers that call the services in this case but other services. In my mind, there is little difference in that what calls the services and services should not care.
So there is no coupling between domain/business modules in my case thanks to this.
You can use whatever technique you want for this mapping tbh. Adapter just works in my case, I think it is nice and simple, just mapping types between calling services.