Monolith First: When One Laravel App Beats a Microservices Plan
Microservices make sense at a certain scale, and almost nobody starts at that scale. A two-developer SaaS with four services has distributed transactions, versioned APIs between its own components, and three different deploy pipelines, all in service of a product that may not find its market. The failure mode is spending the first six months on infrastructure instead of features.
Our default for client projects is one Laravel app, modularized internally. Not because monoliths are fashionable, but because the problems microservices solve are usually problems you don't have yet.
What a modular monolith actually means in Laravel
It doesn't mean one giant app/ directory where controllers talk to models across domains. It means drawing domain boundaries in the code itself and enforcing them with autoload rules or just discipline:
app/
Domains/
Billing/ // subscriptions, invoices, payment webhooks
Projects/ // the core product feature
Users/
Http/ // thin controllers that call into domains
The rule: Billing may use Users, Projects may not reach directly into Billing's models. Everything crosses boundaries through a service class or a dispatched job. In Laravel you can go further with a composer.json PSR-4 mapping per domain and deptrac to fail the build on illegal dependencies. Even without tooling, "controllers live in Http, services live in their domain, models never cross domains" catches most of the rot.
The payoff isn't the directory structure. It's that when the split finally makes sense, the seam already exists.
The three boundaries worth keeping clean
A monolith can let everything share everything. Resist that for exactly three areas, because they're the ones that become painful to untangle later.
- Billing. Payment code changes on its own schedule, fails on its own schedule, and carries your money path. Keep it behind its own service interface even in a monolith. When a client later asks to switch Stripe for a local provider, or to invoice from a separate service, you're moving a class, not performing surgery on checkout flows.
- Notifications and email. Whether the app sends mail directly or queues it for a worker should be a swap of one mailer config, not a rewrite of every feature that wants to notify a user. Route everything through one notification layer from day one.
- Background jobs. Use the queue abstraction even when the "queue" is
sync. The day you need a real Redis queue, and you will, the change is an environment variable.
Everything else can be as coupled as it wants for the first year. The reports page can join five tables directly. The admin panel can call domain services sloppily. That code is cheap to refactor precisely because it stays inside the monolith.
Signs it's time to split something out
The split, when it comes, should be boring and specific. Not "let's go microservices," but "let's move PDF generation to a worker service." The triggers we watch for:
- One part of the app has wildly different resource needs. A report generator chewing through memory doesn't need to scale with the CRUD frontend, and keeping them in one deploy means sizing for the worst of both.
- Different release cadences. If the billing integration needs careful, staged deploys while the product ships daily, separate deploys reduce risk.
- A second codebase needs the same logic. The moment a mobile app or a second product calls your billing logic, extracting it behind an API stops being premature.
- Team growth past what one deploy pipeline can coordinate. This is an organizational limit more than a technical one, and it arrives later than most people expect.
None of these apply to an MVP. All of them can apply to a product two years in. Building as a modular monolith keeps both futures open.
The counterargument we take seriously
Microservices advocates are right about one thing: the monolith can rot. A Laravel app where every feature touches every table, where tests need the whole application bootstrapped, and where deploys are risky because everything is coupled, is a real problem. But the fix for that rot is boundaries and tests, which you can have in a monolith. Starting a greenfield project with eight services is a different decision, and usually a worse one.
The honest comparison is: a modular monolith with three clean seams versus microservices from day one. The first ships features every week and can still split later. The second ships infrastructure. For most teams at most stages, that choice isn't close.