Developing Better Monoliths in Laravel
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.
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.
Note that:
- 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.
- Microservices needs API Gateway and Event Bus as additional components.
- API gateway accepts all API Calls, handles common tasks such as logging, authentication, rate limiting, and statistics and returns response to client.
- An event bus allows communication between microservices by allowing some microservices to act as publisher of events while others as subscribers.
- 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.
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.
Using interfaces
Tight Coupling is a 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:
- Create Service Interface
- Let FooVehicleService implement that interface
- Create a binder that returns service instance implementation against interface
- Use interface in Client Controller instead of implementation
Interface for loose coupling
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
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.
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.
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.
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..
As pointed out by Ryan Rebo, a more evolved version would delegate logic and rules of the application to separate services.
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
- 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”.
- Both Controller and Command are interacting with “GithubUserService” to get the github profile for a given user.
- GitHubService is using the “GithubUserRepo” repository. This repo invokes HTTP-API endpoint https://api.github.com/users/<username> to get user info.
- “GithubUserRepoHttpImpl” is an http implementation of GithubUserRepo (loose coupling). This helps us switch to grpc implementation, for example in the future.
- 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.
Let’s see one by one.
Repository
Service and 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.