Microservices with Laravel - The Problem with Monoliths
In order to understand what microservices are, first, we have to talk about monolith systems and their drawbacks.
Imagine Amazon as a Laravel project. It is one huge git repository, and it’s made of classes like these:
And so on. Just ask yourself: what does the one-click buy functionality have to do with shipping? One-click buy cares about credit card numbers, and user info, while shipping cares about product attributes, like weight and height, and addresses. Similarly refund logic has nothing to do with a wishlist.
Despite the fact that these modules, these classes are very different from each other, they live in the same repository, they can be accessed by the same API, they can call each other, and they can share some general functions or classes.
They are tightly coupled and have low cohesion. But in great architecture, we’d like to have loose coupling and high cohesion.
What the hell do those words mean?
If a project is loosely coupled, a change in one place should not require a change in another place. Imagine you have Product, Order, Discount, and Payment in the same project. When you start the project you have some small, nice classes. The user creates an order with some product ids, you make a query from Product, calculate sum prices, and so on, nice and simple. After a while business wants discounts. So you call the Discount class from Order, and you pass the products. Nice and simple. One day, the business wants a discount based on the total amount of orders. No problem, you just pass the Order object to Discount. Next, you need to give a discount based on the user’s order history and payments, so you pass more orders, and the User object to the Discount, maybe you also want to get something from the Payment module.
And here we are. The tightly coupled monolith. Now you have communication between:
What happens next? You modify the getNetPrice() function in the Product class, and suddenly the Discount API call returns a 500 status code. Or even worse it calculates the wrong amounts. You change something in the User class and Order starts to behave differently. You modify something in OrderHistory and you get the wrong discounts.
This is a tightly coupled system.
You make a change somewhere, and the application breaks in a different place. Oftentimes in a completely unrelated place.
High cohesion means that related behavior sits in one place, and other unrelated behavior sits in a different place. You can interpret this at the level of classes, for example, in the Order class, you don’t want a method called getProductPrice(), you want it in the Product class. Soon, when we actually get to the point of microservices we will talk about cohesion at the level of product or services. But for now, the important part is we want related behavior in one place. And this is because, if we want to change that behavior we want to make sure to not break anything else. So if you have high cohesion, you can make easier, more secure changes in your system.
In the world of monoliths, it is very easy to make high coupling and low cohesion. It comes from the nature of monoliths, you just put a ton of unrelated classes and methods next to each other. And as the number of features, the number of classes, methods, and the number of developers grow, it’s just too easy to make the mistakes I mentioned above.
Don’t get me wrong, you can write good, high-quality monoliths. And in my opinion, every system has some coupling and cohesion problems, no matter how well made it is.
What are microservices?
Imagine a restaurant where the waiter is using a mobile app to take your order. He takes your order, and the chef gets the information about what he has to cook. When the chef is done, the waiter delivers your meal, then you eat it. After that, the waiter prints your receipt via the mobile app. At the end of the day, the manager opens the same system on his laptop and sees the revenue, the net income, the cost of revenue and calls it a day. Every Friday he goes to his favorite supermarket and buys some ingredients, then he tracks them in the system. At the end of every month, he downloads an Excel about financials and e-mails it to the bookkeeper.
In this domain, we have models like:
First, try to create some groups from these classes. In each group let’s put similar classes, which are related to each other:
So we’re grouping the classes based on their functionality. In our application, we have responsibilities, such as:
- Product management
- Keeps track of our products and ingredients
- Order management
- Stores the customers’ orders
- Discount management
- Handles discount cards and so on
- Creates reports about the business
Now let’s put each class into one group:
- Product management
- Order management
- Discount management
So, we basically namespaced our classes right? Yes, and in a classic monolith application, you create one repository and put all the classes in it. But in the microservices world, we create a separate project for each namespace.
What’s exactly a service?
A service is a running Laravel instance, an application on its own, which usually has an API, and can communicate with the outside world. You create these groups, these boundaries around your functionality, and put each group into a different Laravel application with its own API.
It’s not a composer package.
It’s an application. You can start it, stop it, deploy it at any time, call its API, run an artisan command in it, and so on.
What makes it micro?
In software development, size does matter. Usually the smaller, the better. The smaller your function, your class, the easier it is to maintain it. The same concept applies to services.
How small a service is? It’s hard to tell exactly, but there’s a good rule of thumb: it has to be small enough to completely rewrite it in one or two sprints. So if you have a team of 4 and you work in 2 weeks sprints, then you can rewrite the whole service in 2-4 weeks. By rewrite I mean, you delete the git repo and start from scratch. It really depends on the project, on the team, maybe it’s 5000 lines of code for you, maybe it’s 2000 for another team. By the way, the rewrite rule comes from Sam Newman’s Building Microservices.
Alright, so we took our classes, put them into groups, and created 4 different services. How do they communicate with each other? How does the Finance service get the orders and products to calculate revenue? In the next few chapters we will talk about all of our options, but first, we need to talk about these groups a little bit.
Finding bounded context
On the previous pages, we talked about groups and grouping classes. But in the real world, we call these groups bounded contexts. The expression comes from Eric Evans’ book called Domain-Driven Design. Every product has its own domain, and language, like the one in the restaurant example. Every domain has models in it, but you can always find a set of models that can be grouped together and you can draw a boundary around them. This boundary means that these models can live together without any other external dependency.
In every bounded context, there are some internal implementation details, and some internal models which you don’t want to expose to the outside world. But it also has some shared models, which can be shared with the outside world. This is a very important concept. What it means is that you don’t want to expose all of your Laravel Models from the Product service for example. You want to keep these models inside the product service because it has a lot of implementation details, and you want to create some leaner, transformed version for external usage. We will talk about it in more detail later in this book. For now, you can imagine something like this:
Where the green Product box is the internal model, a classic Laravel Model, and the yellow one is an external model, which can be shared with the order service for example. In code, we’ll use DataTransferObjects to represent these shared models.
Finding bounded context is the single most important thing when we talk about microservices. And unfortunately, it is the hardest skill to have. For example in the restaurant example, we have a model where:
- ProductGroup contains products
- A Product is made of ingredients
- An Ingredient is stored in a Storage
And we have these models in the same service, in the Product service. But why don't we have a separate service for the whole storage management? And the answer is because this is just an example. Here’s the thing: products and ingredients are closely related things, so it’s logical to put them in one bucket. But in my experience warehouse management is complicated enough to have its own service. But maybe in this application, it’s a simple thing so we don’t want to complicate things. So it really depends on the product, the domain, and the functionality of our software.
As you may know, I published a book called "Microservices with Laravel." Check it out if you want to learn more!