DEV Community

Tevin Dean
Tevin Dean

Posted on

Why I No Longer Prefer Monolithic MVC

Looking back when I was in college, I used to make apps. Small-scale monolithic projects, hosted on local machines so they can be accessed by my peers. CRUD apps, or probably little calculator stuff, or an app that can post an article. Whether it was on Java or PHP Laravel at that time.

Every project that I used back then always started with an MVC (Model-View-Controller) Design Patterns in mind. Let’s overview what MVC is for a moment.

  • Model: This is what we know as the ORM (Object Relational Models). The models basically map the database tables into a class object.
  • View: The user interface is how the user would interact. (In the backend, we can assume this as URL endpoints of an API; it can also be client requests.)
  • Controllers: The classes that handle the processing of data, whether it was objects, arrays, variables, etc. The controllers become the central of logic blocks.

When I join the tech industry, my first company is in the financial sector. I learn about many things, including microservices. As a startup, this company wants to move its monolithic application into a microservice architecture. My biggest challenge is that we didn’t have much time to research on how to do such a feat.

I was a junior back then, and I barely know about microservices. The managers told me to create various microservices using Laravel Lumen frameworks. It was a mess to decouple the logic within controllers. I create 7 microservices and a “makeshift” API gateway in the span of 8 months. I cannot validate my work to be the “gold” standard of microservices. Post-covid events, the company goes bankrupt and laid off all of their employees.

During my unemployment era, I learned about the DDD (Domain Driven Design) which eventually led me to make another little API project to experiment with Modulit, also known as the Modular Monolith. This makes me know that the MVC is not just the only way to create a software architecture.


Death by Line of Code

I’ve seen many projects that implement MVC models without implementing any abstraction or composition. There’s no dependency injection, no logic, or business layer segregation. Most inexperienced developers like me back then would throw everything into the controllers. In the controller classes, many of us would easily make poorly written code. Which are very difficult to read and to understand.

The if-else, the switch-cases, the magic numbers, and all that SQL Queries happen in the same controllers. Should we follow the Robert C. Martin Clean Code? Where does he do many layers of abstractions? I don’t think we need to. Create readable code by segregating logic and business layers, or maybe 1 or 2 layers of abstraction or inheritance are more than enough.

Imagine if the IDE shows 12000 lines of code within a single controller. It would probably be really hard to read.

Modules and Domains

Have you ever seen that within a project, there’s no difference between the PaymentController and the InvoiceController or any other controllers? It's all in the same /controller folder. Then there’s another business requirement that demands you to create an invoice for shipment and logistics. Then you would start writing InvoiceShipmentController or InvoiceLogisticController

Image description

The structure above will create a total mess because there’s no context of the controllers that belongs to which features or business decision.

Let's say we try to structure this to create context:
Image description

You can see within the Modules folder that each module/domain will have its own models and controllers. You can also add another folder, such as Views, Requests, Validations, or Routes, inside of each module folder.

Don’t Repeat Yourself Principle

For inexperienced programmer, Controller became the processing points of an application. There’s no other sub-classes that handles the process. Some processes are sometimes requires the same code to be implemented. And the easiest shortcut that many of us lazy developer tend to take is to copy-paste the code from an existing controllers to the other controller.

Imagine if InvoiceShipmentController and InvoiceLogisticController had to insert SQL query to the same Invoice Table in the Database. Let’s just say the difference between them is only in the category column which are ENUMS.

  • Category 1 is for Shipment
  • Category 2 is for Logistic
  • Category 3 is for Other

We also wants to make possible changes if there’s another category that might be included in the future.

We can copy paste the code and probably just change the category=1 or category=2 in the insert database models. But instead, try to incorporates sub-processes in other class and then call its method. for example like this:

class InvoiceShipmentController extends InvoiceController
{
    public function __invoke(){

            //call the action
            $action = new InvoiceShipmentAction
            $result = $action->run($model, $payload)

            return $result
    }
}

class InvoiceShipmentAction
{

    // Instantiate InvoiceAction within the constructor
    public function __construct()
    {
        $this->shipmentAction = new InvoiceAction();
    }

    public function run($model, $payload){
        // call SetShipmentCategory
        $this->shipmentAction->SetShipmentCategory($model, $payload['category'])
    }
}

class InvoiceAction
{
    // Main Logic 
    public function run($model, $payload){
        //call SetShipmentCategory
        $this->SetShipmentCategory($model, $payload['category'])

        /* Invoice LOGIC*/
    }

    // THIS WILL ACT AS SUB-PROCESS
    protected function SetShipmentCategory(InvoiceModel $model, int $category){
            if ($category <= 0) || ($category > 3){
                    throw new Exception("Invalid Category!");
            }

            // Update the category
            $model=>category = $category;
            return $model
    }
}
Enter fullscreen mode Exit fullscreen mode

We can do this in many different ways. We can try to use inheritance in InvoiceShipmentAction, dependency injection in the class constructor, or just like what I did in the code, manually callingnew InvoiceAction() in constructor. We can reuse the SetShipmentCategory() in a lot of different ways. We can chop things up to break the code into smaller parts, rather than jam-packed the whole thing inside of the controller.

We can also just declare the category for each individual module, whether it was intended for ShipmentInvoice, LogisticInvoice, or OtherInvoice.

Prepared for Microservices

We don’t always need to build our architecture in microservices; it would be very time-consuming if you wanted to create your little calculator app in a microservice.

Probably right now, your company doesn’t want to adopt microservices because the number of clients is still small and the number of business processes is not even that big. You might even never need to migrate into microservices at all.

But for companies and enterprises, scalability is going to be an issue. Something that we need to prepare before the problem arises. If we planned our projects to have their own modules, we might have an easier grasp to translate the monolithic apps into microservices, or even if we didn’t use microservices, we might have better maintainability over the context over each module that we had created.

Top comments (0)