Laravel monoliths get blamed for problems that are not really monolith problems.
Slow pages, tangled models, messy controllers, and painful deployments are real issues. But splitting the application into services does not automatically fix them. It usually makes the same problems more expensive, because now the code is also spread across networks, queues, logs, deployments, and databases.
A monolith is not a failure state. For many teams, it is the fastest and most understandable shape of the system. The question is not “When do we stop being a monolith?” The better question is: which part of the system has become independent enough that keeping it inside the main application is now the more expensive choice?
Bad reasons to split
The worst splits usually start from frustration, not boundaries.
“The repo is too big” is not enough. Big repositories can be navigated, tested, and organized. A badly organized monolith should usually be reorganized before it is distributed.
“Deploys are annoying” is not enough either. If the deployment pipeline is slow, fix the pipeline first. If tests are unreliable, splitting them into five services will not make them more honest.
“Microservices scale better” is only true when the bottleneck is isolated. If the same database, same transaction flow, and same release process sit underneath everything, the system has not really been split. It has been given more moving parts.
The dangerous version looks like this:
Old problem: one Laravel app is hard to change
New problem: five Laravel apps are hard to change together
That is not architecture. That is distribution without ownership.
Good reasons to split
A service boundary starts to make sense when the area has a different reason to change than the rest of the application.
The strongest signals are usually these:
- The data has a clear owner.
- The workload scales differently.
- The failure mode should be isolated.
- The release cadence is different.
- The contract can be described without sharing internal models.
Billing is a common example. The billing system has its own rules, audit needs, failure tolerance, and data ownership. A marketing page should not be able to casually change invoice behavior because both happen to live in the same repository.
Notifications can be another candidate. Email, SMS, push, retry rules, provider failover, and delivery logs often grow into their own operational problem. If notification failures should not block checkout, onboarding, or profile updates, that boundary deserves attention.
Search, reporting, media processing, fraud checks, device ingestion, and matching systems can also become candidates. Not because they sound like services, but because they have different load patterns and different failure expectations.
Use Laravel boundaries before service boundaries
Laravel gives you several ways to separate behavior before you reach for another deployable service.
Queues move slow work out of the request path. Events let one part of the system announce something happened without directly calling every side effect. Jobs, listeners, actions, policies, form requests, API resources, and dedicated service classes can all reduce coupling inside one application.
That should be the first move.
app/
Domains/
Billing/
Actions/
Events/
Jobs/
Models/
Policies/
Notifications/
Actions/
Events/
Jobs/
Services/
This is still a monolith, but it is no longer a pile. The code has local boundaries. You can see which models belong to which domain. You can keep controllers thin. You can test behavior without walking through the entire application.
If a domain cannot be kept clean inside the monolith, it probably will not become clean just because it moved to another repository.
The database is the real split
Extracting code is the easy part. Extracting data is where the architecture becomes real.
If a new service reads and writes the monolith database directly, the split is mostly cosmetic. The code moved, but the ownership did not. Every schema change still requires coordination. Every query can still reach into another domain’s tables. Every deployment still carries hidden coupling.
A real split means the service owns its data and exposes behavior through a contract:
Weak boundary:
Billing service reads users, orders, invoices, and subscriptions from the monolith database.
Stronger boundary:
Billing service owns invoices and subscriptions.
The monolith calls Billing through an API or event contract.
Billing publishes billing status changes back to the rest of the system.
That does not mean you move the database first. Usually you do the opposite. Isolate the code. Define the events. Add queues. Make writes idempotent. Remove direct model reach-through. Then move data when the boundary is already proven.
The database should be the last thing you split, not the first thing you improvise.
Synchronous calls are a warning sign
Some behavior needs a direct response. Payments, authentication, permission checks, and availability checks often sit in the request path because the user cannot continue without the answer.
But if every new service call is synchronous, the system becomes fragile quickly.
User request
-> Laravel app
-> Billing service
-> Notification service
-> Reporting service
Now the request is only as reliable as the weakest dependency in the chain. Latency accumulates. Retries become dangerous. Timeouts become product behavior.
Before extracting a service, decide which communication must be synchronous and which can be event-driven. A good split usually moves side effects behind jobs and events first. The user action records the important state change, then the rest of the system catches up.
That is slower to design, but easier to operate.
A practical extraction path
The safest splits are boring and incremental.
Start by naming the domain inside the Laravel app. Move behavior into a clear folder. Stop letting controllers reach across the whole application. Create actions for writes and query objects or repositories where reads have become messy.
Then define the contract. What does this domain receive? What does it emit? What does it own? What should other parts of the application never touch directly again?
Next, move side effects to jobs and events. Make jobs idempotent. Give them retries, timeouts, and useful failure handling. A service boundary without idempotency is a production incident waiting for the first retry storm.
After that, put the domain behind an interface:
interface BillingGateway
{
public function createSubscription(UserId $userId, PlanId $planId): SubscriptionResult;
public function cancelSubscription(SubscriptionId $subscriptionId): void;
}
At first, the implementation can still call local Laravel code. Later, it can call a separate service. The rest of the application should not care. That is the point of the boundary.
Only after the contract is boring should you extract the deployable service.
What I would keep in the monolith
Not every domain earns independence.
Admin CRUD usually belongs in the monolith. Basic user profiles usually belong in the monolith. Small settings screens, simple content management, internal dashboards, and low-volume workflows usually do not need their own service.
The cost of extraction is paid forever:
- Separate deployment
- Separate logs
- Separate monitoring
- Separate secrets
- Separate local development setup
- Separate failure modes
- Network calls instead of function calls
- Versioned contracts instead of direct refactors
That cost is fine when the boundary is strong. It is waste when the boundary is vague.
The checklist
Before splitting a Laravel monolith, I want clear answers to these questions:
- What data will the new service own?
- Which tables will other systems no longer touch directly?
- Which operations are synchronous, and why?
- Which operations can move to events or queues?
- What happens when the service is down?
- Can the domain be tested in isolation today?
- Can it be deployed independently without coordinating every release?
- Is the team ready to monitor and operate another production system?
If those answers are weak, keep the code in the monolith and improve the internal boundary first.
The lesson
Split a Laravel monolith when the business boundary is clearer than the framework boundary.
Until then, use the monolith well. Organize by domain. Keep controllers thin. Push slow work to queues. Use events for side effects. Protect data ownership even before there is a network boundary.
A good monolith can become a good set of services later. A confused monolith usually becomes a confused distributed system.