Sharing data between modules in modular monolith

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 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. 

Creating logical coupling

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.

Approach with BFF

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.

Frontend app getting the data from different modules’ APIs

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.

Populating the data between modules

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?

Let's stay in touch!