At stomt we collect constructive feedback, boiled down to single arguments, recognize duplicates and merge them into movements.
In this blog post, we show a way to split up large Laravel applications into smaller micro-services, in our case Laravel & Lumen applications, and the advantages and pitfalls it brings with it. As a result, we sped up our applications by more than 30% and achieved greater maintainability, too. These principles can, of course, be easily applied to other frameworks.
Why do we want to share components between Laravel and Lumen instances?
Our backend was a huge Laravel application. We had a server-side rendered administration interface, a REST API for our clients, an OAuth 2.0 server, as well as our own URL-Shortener (stomt.co). In addition, we had a queue backend used for image resizing and other intensive tasks that shouldn’t bother our clients. I don’t want to say that Laravel is slow but we were searching for even better performance and when Lumen came around the corner, we decided to split our backend into multiple micro-services. That would make it easier to scale it across several Amazon Web Services and would also allow to give certain features better and more individual maintainability.
A lot of functions are needed in our REST API as well as in our administration panel. But while the REST API results sometimes were not enough and special behavior is needed from time to time we couldn’t rely 100% on just making API calls from one service to another. At this point we needed to be able to call commands/jobs from Laravel (used by our administration panel) as well as Lumen (our REST API).
While we still have a “monolithic” codebase, we have multiple completely independent backend applications. You might want to call them “micro-services” (because it’s so trendy).
Requirements when splitting up the application
How to make sure that components, especially commands, configs and service providers can be used across different services when each service has its own namespace?
- Start using components (we will get into this later)
- Make use of Event Subscriber classes and register them (thereby each component should have its own Event Subscriber class)
- Make sure that you have all business logic in commands (now called “Jobs”) or something similiar. It is important to encapsulate all business logic in your components. Your commands have no access to data from outside. You do this in your HTTP layer (in Controllers) and pass the data.
- Use namespaces according to your folder-structure everywhere. (PSR-4 recommended)
- Make sure that things have mostly a single responsibility. The more you do that the more flexibility you have with your shared components.
Splitting up the application
Software evolves over time. You won’t start your startup developing a Domain Driven Design for your backend. You start with a prototype and you let your architecture evolve or do a rewrite. And then you should continuously have in mind to keep your technical debt low. Right from the beginning we at least decided to build our backend component based. A modular structure with a low level approach of SOLID principles.
Let’s take a look how a component looks like:
While Routes and Controllers are staying in the HTTP-layer, components include all the business logic which is separated in commands (in the meantime called “Jobs” in Laravels language), resulting events and their handlers as well as entities/models/repositories. Component folders can also have sub-components. Components can also communicate with each other.
Example: Posts on our web platform are called “stomts”. Let’s say stomts can have comments. You would create a sub-component called “Comment” and place it into the Stomt-component folder. The comment-folder would have the same structure of sub-folders.
How to structure the new backend
Basic idea: The apps folder contains multiple Laravel/Lumen instances.
Anything related to deployments: ElasticBeanstalk files, configs, migrations and seeds, environment-files and scripts are based in the root directory of the backend. The shared folder contains our shared components and service providers. We will dig into this soon.
Release.sh for example automatically creates a new git flow release including semantic versioning and regeneration our API documentation via aglio.
Update.sh calls dependency updating composer commands in all services/apps.
Now let’s dive one step deeper.
Take a close look at the api-application folder. We are always referencing to the root .env-file while using symlinks to reference the configs and database folders within the root-folder. This way configs can be easily maintained across all services as each config file allows multiple configurations. This may be Laravel specific. You can also see what we store in our shared folder: Components, Providers and Services. We will explore how to make service providers shareable later on. Let us first take a look on how to make the shared-folder accessible by the proper use of namespaces and Composer.
Autoload multiple namespaces for shared components
Now, what do we do about namespaces? Composer makes it pretty easy to define PSR-4 namespaces. While each of our application has its own Namespace (e.g. StomtApi for our REST API) and Configfiles don’t need them, our Components folder has an own Namespace called Stomt.
We make those files accessible by using composers autoload configuration:
Pretty easy, hm? Right now we are always deploying the full monolithic codebase while having the advantage of slim and more maintainable applications. More performance by being able to use Lumen for our REST API and the full fledged Laravel framework when needed.
How to share ServiceProviders across Lumen and Laravel applications
There is a small pitfall if you want to make ServiceProviders accessible between Laravel and Lumen applications. They both extend a different EventServiceProvider.
Lumen extends from:
While Laravel extends from:
But if you take a look inside, both are doing exactly the same thing. Laravel is using the Event facade while Lumen is using app(‘events’) to obtain an Event-instance because in Lumen, Facades are disabled by default. To ensure that the ServiceProvider system still works, Lumen makes use of app(‘events’). As the Lumen way would also work in Laravel, the solution is obvious. We’ll copy Lumens EventServiceProvider into the shared Providers-folder and rename the file into “ServiceProvider”. Then we let all Service Providers extend our new ServiceProvider. (You can’t call it “EventServiceProvider” as you normally use the EventServiceProvider to register Event Subscribers in your applications)
In general: If sharing service providers makes sense totally depends on your applications. For us it does and therefore we wanted to share how this can be done.
The long-term goal is to get components more and more framework agnostic. Regarding the Laravel-specific Facades discussion: We don’t use them anymore.
Which parts of our application will we transform into own services next? Auth and Search.
Please feel free to make suggestions and ask questions on Medium or HN!