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

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

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

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

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

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

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

/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

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store