How MVC Frameworks Taught Us Bad Habits — Part 2 How Monoliths are Born

Sergey Radzishevskii
7 min readAug 17, 2020
Photo by Holger Link on Unsplash

This is the second part of the series “How MVC frameworks taught us bad habits.”

This time we will investigate why the default MVC Framework architecture does not always work well. We will trace the path of the application from the MVP to the monolith. Finally, think about what we can do to avoid the difficulties of maintaining our application

MVC Complexity Balance

When we talk about MVC frameworks, we usually supposed such a picture.

And the default folder structure will look like this

- src
- controllers
- models
- views

If you need to add the ability for a user to create a post you will add corresponding сontroller, model, and views.

This separation gives the false impression that models, controllers, and views are roughly equal parts of the system. In large applications, adding a post will very quickly acquire additional logic. Very soon you will find that within the “add post” task there are a dozen related subtasks. You will start to consider a single operation as a use case.

And instead of 3 equal parts, you will get only 2 where controllers will be negligible compared to the business logic.

MVC after N years

Wait where are the views? In React/Angular/Vue 😎. Perhaps for historical reasons you still use “views” but they will be as negligible as controllers. The main difficulty will be in the models.

Architecture

And what about architecture. Frameworks do not force us to take one or another approach when we design architecture. Вut very often developers just take the default structure that frameworks provide and just start extending it. Very soon you find out that you have 200+ models. And then you figure out that something is wrong.

Let’s take a more refined approach. I think it is pretty typical.

MVC Layered architcture

We have architectural layers. Dependencies are directed from the upper levels to the lower ones. Controllers know about services, services know about repositories, and repositories know about the database. But not vice versa.

This architecture looks much better. Dependencies are directed in one direction. We have horizontal architectural boundaries between layers that prevent coupling. And all the business logic is located in the business layer.

What happens if your project continues to grow?

How Does a Monolith Appear

The above approach can work fine for a long time. If your application continues to grow, after a certain point, you notice that the business layer has swollen to a critical size. And now the logic that was clear and simple not so long ago has become more complicated inside the services and it is getting harder to maintain them. You understand that your application has turned into a monolith.

layered architecture after N years

For completeness, scale this picture horizontally 100 times. The service layer becomes the biggest part. Controllers and Repositories are still relatively small in comparison with Service. But the services got bigger. Often, the main services turn into giant monsters. If you measure the cyclical complexity of such services, you will be horrified. The number of dependencies in services has increased, and thereby significantly increased the coupling of services. Services are becoming more fragile.

What usually happens next? The team decides to rewrite the application using microservices.

What are the Benefits of Using Microservices

Let’s see why we think microservices can make our life easier. Usually, when an application reaches the size of a monolith, the team already clearly sees that the application can be split into several fairly independent services.

Why is it beneficial to us? For sure, using microservices will bring us some infrastructure benefits. (Although this will also require additional efforts). But the main advantage is the fight against complexity.

When we pick out the “payments” microservice, we encapsulate the implementation details. When you call a microservice to process a payment from the main application, you don’t think how the payment will be processed, what data will be saved, how that service handles errors, etc. (Of course, if you have to work with the “payments” service the next day, you will need to understand the details). But as long as you call it from the outside, you don’t worry about the details.

This vertical separation clarifies the logic between both the existing monolith and the microservice. We will remove internal dependencies and make the monolith and the microservice communicate via a well-defined interface(either via HTTP or events). In fact, we build architectural boundary, that increase cohesion, and decrease coupling.

Cohesion is an indication of the relationship within a module.

Coupling is an indication of the relationships between modules.

cohesion vs coupling

and definitely it is a very good move.

What is the Problem with Frameworks

Frameworks never encourage you to split logic from the beginning. The path described above is the standard path for many applications. At the same time, isolating microservices from a monolith is a painful and expensive process.

When a team starts breaking down monoliths into microservices, it often doesn’t change their approach. If the microservice turns out big enough (remember microservice — not necessarily small), in the end, the team gets the same complexity problems that were in the monolith.

“If you can’t build a well-structured monolith, what makes you think microservices is the answer?”

Simon Brown

What Can We Improve

Now imagine that you need to create eCommerce application. You realize that it will be a fairly large application with complex business logic. From the beginning, you think about the right, clean architecture. Instead of doing

/src
/controllers
/services
ProductsService
OrdersService
...100+ other services
/models

you separate modules from the beginning

/src
/application
/controllers
/security
/orders
OrdersService
/payments
PaymentsSerevice
PaymentsRepository
/users

We keep controllers separately since they don’t belong to the business logic. It’s application logic — we can communicate via controllers or events or even with CLI application you still can call your services in the business logic. We will talk about the separation of application logic and business logic in the next part of that series.

Business is booming and your company grows. One day, the management decides to allocate a special team that will deal with payments. The development team decides to move the entire payment code into a separate microservice. Let’s estimate the efforts that we will need to spend on this task.

We will need to move our /payments code to the new microservice, we will need to take care of communication between services, logging, failover, circuit breaker, etc — standard list for microservices.

But if we did everything correctly and from the very beginning created our architecture with the correct architectural boundaries — we will not need to spend efforts to change the rest of the modules of our application.

class OrderService {
public Order create(...) {
// validate
// calculate amounts and commisions
const metadata = {
orderId
}
// what happens in vegas stays in vegas
try {
const transactionId = this.paymentsService.pay(amount, metadata);
} catch (PaymentException e) {
...
}
}
}

You don’t really know if `paymentsService` is a class in a monolith or a client for a microservice. Do I need to change something in this code to start working with the payment microservice rather than the payments module? No. This code remains untouched.

Of course, you can argue and say that we could pull out the necessary services, models, entities, but at the same time, the other modules/services did not need to be modified.

/src
/controllers
/services
ProductsService
OrdersService
...100+ other services
/models

As practice shows, when everything is mixed in one place according to the rule — what it is, and not according to what it does, very quickly our classes become tightly coupled. When the boundaries are not delineated from the very beginning, developers begin to mix components from different subsystems.

Conclusion

If you have business requirements for a new application, you cannot reduce them. And thus, it is impossible to reduce the overall complexity. The best we can do is just balance the complexity between the different parts of the application (systems, layers, modules, components, classes), preventing over-complexity in one place. When you start designing application architecture, it is not easy to predict which component will be the most complex. Therefore, we strive to make our application architecture flexible enough that we can easily change it if we need it. Let’s choose our approaches wisely. Let’s take those approaches that allow us to keep our application simple and flexible enough.

--

--

Sergey Radzishevskii

Enjoy turning complex systems into intelligible architectures chilling in my garden.