The Most Common Laravel Mistakes I Still See in Production Apps
Laravel is elegant, expressive, and powerful. It’s also forgiving in a way that can quietly let bad habits survive all the way into production.
What follows is not a list of beginner mistakes. These are patterns I still see in real, shipping applications—apps with users, revenue, and pager duty. Most of them come from good intentions. All of them eventually cost time, money, or sanity.
Let’s walk through the usual suspects.
1. Business Logic Living in Controllers
Controllers should be traffic cops, not decision-makers.
Yet it’s common to see controllers doing things like:
- Calculating totals
- Applying discounts
- Coordinating multiple models
- Calling external services directly
This makes controllers:
- Hard to test
- Hard to reuse
- Easy to break during “quick” changes
Better pattern:
Move business logic into:
- Service classes
- Action classes
- Domain objects
Controllers should mostly validate input, call a service, and return a response. If a controller method feels “long,” that’s usually your cue.
2. Silent N+1 Queries
Laravel makes relationships feel effortless. That’s both a gift and a trap.
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count();
}
This works. It just also runs one query per user.
In production, this shows up as:
- Random slowness
- Spiky database load
- “It’s fast locally” confusion
Better pattern:
Always think in sets.
$users = User::with('posts')->get();
If you’re looping and touching relationships, eager loading should be a reflex.
3. Treating .env as Runtime Configuration
.env is for boot-time configuration, not application logic.
Mistakes I still see:
- Reading
env()directly outside config files - Changing
.envvalues and wondering why nothing happened - Conditional logic based on
env()values
This breaks when:
- Config caching is enabled
- You deploy to multiple environments
- You scale horizontally
Better pattern:
- Read from
config() - Cache configuration in production
- Treat
.envas write-once-per-deploy
If php artisan config:cache scares you, that’s a smell worth investigating.
4. No Caching Strategy (or Accidental Over-Caching)
Caching isn’t just Redis “somewhere.”
Common issues:
- No caching at all
- Caching everything without invalidation
- Mixing request cache, model cache, and view cache randomly
This leads to:
- Stale data bugs
- Cache clears as a “fix”
- Fear of touching cache-related code
Better pattern:
Be intentional:
- Cache expensive queries
- Cache derived data
- Define clear expiration or invalidation rules
Caching should reduce work—not introduce mystery.
5. Jobs That Should Be Queued (But Aren’t)
If a request:
- Sends emails
- Calls third-party APIs
- Processes files
- Generates reports
…and it happens inline, you’re borrowing time from your users.
Symptoms:
- Slow requests
- Timeouts under load
- “It only happens sometimes”
Better pattern:
Default to queues.
- Queue emails
- Queue notifications
- Queue heavy logic
Then monitor them. Laravel Horizon exists for a reason.
6. No Observability Until Something Breaks
Many apps go to production with:
- No logging strategy
- No error tracking
- No performance visibility
Then the first question during an incident is:
“Can we reproduce this?”
Better pattern:
Before production:
- Centralize logs
- Track exceptions
- Measure slow queries and slow requests
You don’t need perfection. You need signals.
7. Overusing Magic (Because It’s Convenient)
Laravel’s magic is powerful:
- Model events
- Global scopes
- Accessors everywhere
- Implicit behavior
Used sparingly, it’s beautiful. Overused, it becomes invisible complexity.
Symptoms:
- “Where is this value coming from?”
- “Why does this query behave differently?”
- Fear of refactoring
Better pattern:
Prefer explicit behavior in critical paths. Magic should surprise less, not more.
8. Skipping Tests Because “It’s Just CRUD”
CRUD is where bugs hide.
Common rationales:
- “It’s simple”
- “We’ll add tests later”
- “Manual testing is faster”
Until:
- Validation rules change
- A relationship changes
- A refactor breaks assumptions
Better pattern:
At minimum:
- Feature tests for critical flows
- Tests around business rules
- Tests that lock in expectations
Tests aren’t for confidence. They’re for change.
9. Not Using Database Constraints
Laravel validation is great. It is not a substitute for the database.
Common omissions:
- Missing foreign keys
- No unique constraints
- Nullable columns that shouldn’t be
This leads to:
- Orphaned data
- Subtle bugs
- Cleanup scripts nobody wants to write
Better pattern:
Let the database protect itself.
- Use foreign keys
- Enforce uniqueness
- Be explicit with nullability
Laravel works with the database, not instead of it.
10. Growing Without Architecture Re-evaluation
The app that starts as:
“Just an admin panel”
Often becomes:
- A public API
- A mobile backend
- A multi-tenant system
But the architecture stays frozen in “v1 thinking.”
Better pattern:
Periodically ask:
- Does this structure still fit?
- Are responsibilities clear?
- Where are the seams?
Refactoring is not failure. It’s maturity.
Final Thought
None of these mistakes mean someone is a bad developer. They usually mean the app succeeded faster than its structure.
Laravel lets you move quickly. The real skill is knowing when to slow down and clean up.
If you recognize a few of these in your own codebase, congratulations—you’re building something real.
The next step is making it durable.
Member discussion