Developing Better Monoliths in Laravel

Manish Sharma
7 min readAug 11, 2023

--

Developing Better Monoliths
Developing Better Monoliths

Keep it loosely coupled, Keep it layered, Provide separation of concerns.

What is Monolithic architecture ?

Monolithic architecture is a software design approach in which all aspects of application are woven together to form a big fat module and therefore developed, deployed, and maintained as a single unit.

Monolithic architecture
Monolithic architecture

This is in contrast to microservices architecture where aspects are partitioned into several loosely coupled independent services that may be deployed and maintained independent of each other. Microservices communicate with each other using event bus or message queue.

microservices architecture
microservices architecture

Note that:

  1. Monolithic systems may have the concept of services. However since all services are packaged as a single unit, it is difficult to modify or deploy or scale them independently.
  2. Microservices needs API Gateway and Event Bus as additional components.
  3. API gateway accepts all API Calls, handles common tasks such as logging, authentication, rate limiting, and statistics and returns response to client.
  4. An event bus allows communication between microservices by allowing some microservices to act as publisher of events while others as subscribers.
  5. Aspects like caching, elastic-search, notifications etc may be woven into monolithic Apps as well.

Should we use monolithic architecture or microservices architecture ?

Monolithic applications are easy to develop, deploy, test and debug, provided we are following design principles such as separation of concerns and adopt layered architecture.

If we want to scale applications or some services as per demand; microservices architecture makes sense. Microservices are easy to scale, maintain, deploy and provide better fault isolation.

If we want to create MVP or an application that does not require considerable scaling in the beginning, monolithic architecture makes sense. The idea is to create a monolithic application keeping layered architecture, loose coupling, SOLID principles and separation of concerns in mind. This will allow easy transition from monolithic to microservices (Strangler Pattern) in future.

Bottom Line : Start with “better” monolith keeping in mind transition to microservices in future. If you are constrained by time, money and Team, start with Monoliths.

Basic concepts

Service

A service is a loosely coupled stateless component designed to solve a problem or perform a specific task and has the capability of communicating with other components of the system, thus creating a composite application. Service components should be reusable, replaceable, context independent (as much as possible) ,extensible and independent.

Loose Coupling

Service Components aka “Server Component” are designed to provide some kind of service to “Client Components”. For example we can design a “GithubUserService” component designed to provide github-user details to a Controller. Such a Controller may be termed as a client component.

Concept of Server Components and Client Components
Concept of Server Components and Client Components

Service Components should be designed with loose coupling in mind. Coupling is the extent of dependency of one component on another. Loose coupling (minimum dependency) is a good idea as it allows components to be developed and used independent of each other. Loose coupling is achieved by using interfaces. This means instead of Component-X and Component-Y talking to each other, they communicate using a common interface I. This interface is implemented by Server Component and Client component interacts with Server Component using interface I. This allows Server Components to be updated or modified without notifying Client Components.

Tight Coupling/ Direct Interaction is a bad idea.
Tight Coupling/ Direct Interaction is a bad idea.

Using interfaces

Loose Coupling/ Interface based separation is a good idea.
Loose Coupling/ Interface based separation is a good idea.

Tight Coupling is a bad idea

Service Component
Client Component directly interacting with Server Component: Bad idea

Loose coupling/ interface based separation is cool

Imagine FooVehicleService being used at several places in the App, and later we want to replace FooVehicleService with BarVehicleService. We have to do changes at many places. Clearly we are breaking DRY(Don’t repeat yourself) rule. To solve this, let’s use loose coupling as follows:

  1. Create Service Interface
  2. Let FooVehicleService implement that interface
  3. Create a binder that returns service instance implementation against interface
  4. Use interface in Client Controller instead of implementation

Interface for loose coupling

Interface for loose coupling

Implementating class

Implementating class

Binder

Using interface instead of implementation in client

Now if implementation changes, we just have to update Binder without notifying Client Component.

if(self::$instance ==null){
self::$instance =new BarVehicleService();
}

Dependency Injection

Normally if Component-X requires Component-Y , Component-Y is instantiated by Component-X for use. This makes X directly dependent on Y.

What if we want to provide loose coupling ?

What if we want easy testing where Component-X may be tested independent of Component-y ?
The solution is dependency injection.

Dependency injection provides inversion of control by allowing an agent “Z” such as “Container” or “Binder” to inject “Y” into “X” via constructor or other mechanism.

Laravel provides service container for managing dependency injection.

Some examples are

If we are injecting a Service class variable inside constructor of a Client Component, service container will automatically instantiate Service Component upon instantiation of Client Component

Dependency injection via constructor for Class

If we are injecting a Service interface variable inside the constructor of a Client Component, the service container will instantiate the implementation component using appropriate bindings.

Dependency injection via constructor for Interface

All that we have to do is to add a line to register() method of App\Providers\AppServiceProvider as follows:

$this->app->bind(GithubUserRepoInterface::class, GithubUserRepoHttpImpl::class);

This will tell the Service container to instantiate the GithubUserRepoHttpImpl class if GithubUserRepoInterface is injected.

Separation of concerns

Separation of concerns is to divide an application into components separated by boundaries of responsibilities. For example it’s a bad idea to keep validation logic, database access logic, presentation logic etc at the same place. We should create separate services for each and glue them together inside the Client component. Separation of concerns is an important step in designing applications that follows layered architecture.

Consider this Spaghetti Code (unstructured and difficult-to-maintain source code) that does not take Separation of concern into mind.

Spaghetti Code is a nightmare
Spaghetti Code is a nightmare

What if Database interaction or Business logic spans over multiple lines ?

What if the same Business logic is required elsewhere ?

Aren’t we breaking the DRY rule ?

Is this code easy to maintain ?

The solution lies with separating the concerns.

As shown in the diagram, concerns are separated and spaghetti code is replaced by respective service invocations.

Separation of concerns
Separation of concerns

MVCS (Model View Controller Services) : MVC on Steroids

Laravel follows MVC design pattern to provide separation of concerns as follows:

Model: is responsible for managing data, logic and rules of the application.

View: representation of information as response to client

Controller: is a request processor. It accepts input from the client, interacts with the model and passes the result to view..

MVC Design Pattern
MVC Design Pattern

As pointed out by Ryan Rebo, a more evolved version would delegate logic and rules of the application to separate services.

Model View Controller Services Design Pattern
MVCS Design Pattern

We should thin out or clean up our fat models for code readability. It is better to create services, each handling a separate concern. Service objects are a great resource to help improve the readability of your code and keep things with only one responsibility.

Let’s start building

The App we are designing

  1. There is a Controller(GithubUserController) to handle http requests and a Command (GetGithubUserCommand) accepts user input. In both cases “username” is input and attributes “bio”,”html_url” and “group” are the output. Here “group” is a computed attribute. if followers>1000, the group is “Expert” else “Novice”.
  2. Both Controller and Command are interacting with “GithubUserService” to get the github profile for a given user.
  3. GitHubService is using the “GithubUserRepo” repository. This repo invokes HTTP-API endpoint https://api.github.com/users/<username> to get user info.
  4. “GithubUserRepoHttpImpl” is an http implementation of GithubUserRepo (loose coupling). This helps us switch to grpc implementation, for example in the future.
  5. Once Github service gets data from GithubUserRepo, it applies transformation using PHP-Fractal library and returns response to client. Fractal provides a presentation and transformation layer for complex data output, thus acting as a “barrier” between source data and output, so schema changes do not affect users.
App based on Services : Skinny Models, Skinny Controllers, Fat Services
App based on Services : Skinny Models, Skinny Controllers, Fat Services

Let’s see one by one.

Repository

Repository interface
Repository implementation

Service and Transformer

Service using Repo
Fractal Transformer

Http Controller as Client Component

Console Command as Client

api.php (route for Controller):

Route::get('/github-user', [GithubUserController::class, 'getUser']);

Thus Http-client may be invoked as:

http://127.0.0.1:8000/api/github-user?username=twitter

And Console command may be invoked through terminal as:

 php artisan app:github-user twitter

That’s it. Still there is lot of scope of improvement. Let’s evolve together.

You may download source from this repo.

The next part of this tutorial explaining Observer Pattern is here .

Happy Coding.

--

--