How I Structure a Laravel App That’s Meant to Grow
Most Laravel apps don’t fail because of scale.
They fail because they become difficult to change.
Files drift. Responsibilities blur. Logic migrates into places it never belonged. Six months later, every feature feels risky — not because it’s complex, but because the structure no longer communicates intent.
This article isn’t about chasing perfect architecture. It’s about building a Laravel app that can absorb growth without fighting you.
The Core Philosophy
I structure Laravel apps around three guiding principles:
- Optimize for change, not elegance
- Make intent obvious in the filesystem
- Delay abstraction until pressure demands it
Laravel already gives us strong defaults. The mistake is either abandoning them too early — or never evolving them at all.
Start With Laravel’s Defaults (On Purpose)
Laravel’s default structure is not naive. It’s intentionally flat and approachable:
app/
Models/
Http/
Controllers/
Middleware/
Providers/
Early on, I lean into this.
Controllers stay thin. Models stay boring. Business logic lives close to where it’s used.
The key rule at this stage:
If a class can’t clearly justify its own folder, it doesn’t get one yet.
Introduce Domain Folders When Patterns Repeat
Growth creates repetition. That’s your signal.
When I see related controllers, requests, actions, and services clustering around the same concept, I introduce a domain‑centric structure:
app/
Domains/
Billing/
Calendar/
Auth/
Each domain owns its behavior:
Domains/
Calendar/
CalendarController.php
CalendarService.php
CreateEvent.php
UpdateEvent.php
CalendarPolicy.php
This isn’t DDD theater. It’s simple ownership.
If you need to understand calendar behavior, you open one folder — not six directories across the app.
Controllers as Traffic Cops, Not Brains
Controllers should orchestrate, not decide.
A healthy controller method usually looks like this:
public function store(CreateEventRequest $request)
{
return CreateEvent::run($request->validated());
}
If a controller grows logic branches, conditionals, or data massaging, that logic graduates into an action or service.
When controllers stay predictable, refactoring becomes safe.
Prefer Explicit Actions Over Generic Services
I favor use‑case‑named actions over generic service classes.
This:
CreateEvent
UpdateEvent
CancelEvent
Is easier to reason about than:
CalendarService::handle()
Actions give you:
• Clear intent
• Easier testing
• Natural extension points
They scale horizontally without becoming abstract soup.
Models Stay Focused on State
Eloquent models already do a lot. I avoid turning them into god objects.
Models should primarily handle:
• Relationships
• Attribute casting
• Query scopes
Business workflows belong elsewhere.
If a method reads like a sentence, it probably doesn’t belong on the model.
Requests, Policies, and Validation Live Near Their Domain
Validation and authorization are part of behavior.
When domains emerge, I move related artifacts with them:
Domains/
Billing/
Requests/
Policies/
This avoids the "global junk drawer" problem where unrelated logic coexists simply because Laravel allows it.
Don’t Over‑Abstract Early
Interfaces, repositories, and adapters are powerful — when justified.
They are also maintenance debt when premature.
I introduce abstraction when:
• I have two real implementations
• I expect churn
• Tests become painful without it
Not because a blog post told me to.
Tests Mirror Structure
Tests follow the same domain layout:
Tests/
Feature/
Calendar/
CreateEventTest.php
When structure mirrors intent, tests become documentation.
What This Buys You Long‑Term
This approach gives you:
• Predictable growth paths
• Easier onboarding
• Safer refactors
• Fewer architectural rewrites
Most importantly, it keeps momentum intact.
Laravel is already productive. Your structure should amplify that — not fight it.
Final Thought
A scalable Laravel app isn’t defined by how clever its architecture is.
It’s defined by how confidently you can change it six months from now.
Structure is a communication tool. Make it speak clearly.
Member discussion