ORIGINAL ESSAY / SYSTEMS AND PEOPLE
How to Write a Good Symfony Project (That Will Still Make Sense in 5 Years).
Most Symfony projects don’t collapse because of performance.
They collapse because of entropy.
After working on long-lived systems, I’ve settled on a few structural principles that dramatically improve clarity, maintainability, and correctness.
This is not about trendy architecture patterns.
It’s about controlled complexity.
---
1️⃣ Structure First. Patterns Later.
Forget DDD, Hexagonal, Onion, or whatever geometry is fashionable this year.
Start with a structure that makes navigation predictable:
include/ → Definitions (contracts, DTOs, enums, constants)
lib/ → Reusable logic (framework-agnostic)
src/ → Symfony runtime integration (controllers, services, adapters)
Rules:
- include depends on nothing.
- lib depends only on include.
- src depends on both.
When someone opens the project, they should instantly know where to look.
Architecture should reduce cognitive load, not increase it.
---
2️⃣ DTO First. DTO Last.
Golden rule:
- DTO first for request
- DTO last for response
Every request should:
- Be mapped to a DTO
- Be normalized
- Be validated
- Be rejected early if invalid
All of this should happen before business logic runs.
If something reaches your controller, it should already be valid and usable.
Controllers should effectively look like:
RequestDTO → Service → ResponseDTO
No raw request parsing. No scattered validation. No defensive programming inside business logic.
Invalid data should never reach the core.
---
3️⃣ Use the Symfony Request/Response Pipeline Properly
If something affects most endpoints (headers, correlation IDs, error shaping, caching policies, security headers, etc.), don’t inject it everywhere.
Use:
- Event listeners
- Value resolvers
- Kernel hooks
Apply cross-cutting concerns at the edges, not via dependency chains.
Add clear exclusion rules where needed (health routes, profiler, internal endpoints).
The framework already gives you a pipeline. Use it.
---
4️⃣ Use Attributes (Symfony ≥ 7.3)
Keep configuration close to code:
- Routing via attributes
- Doctrine mapping via attributes
- Event listeners via attributes
- Message handlers via attributes
Configuration files should describe environment, not wiring.
This keeps intent local to the class and prevents configuration sprawl.
---
5️⃣ Never Let Invalid Data Reach Business Logic
Validation belongs at the boundary.
Business services should never need to ask:
- “Is this null?”
- “Is this field valid?”
- “Did the client send something weird?”
If they do, your pipeline is leaking.
When boundaries are enforced properly, business logic becomes smaller, clearer, and more deterministic.
---
6️⃣ Accept the Micro-Overhead
DTO hydration and validation add a small processing cost.
But they massively increase:
- Predictability
- Testability
- Refactor safety
- Long-term maintainability
In real systems, correctness beats shaving microseconds off request plumbing.
Bugs cost more than DTOs.
---
7️⃣ This Doesn’t Break Clean Code. It Enforces It.
This approach naturally reinforces SOLID principles:
Single Responsibility Controllers adapt transport. Services execute business logic. Listeners handle cross-cutting concerns. DTOs represent structured data.
Open/Closed Cross-cutting behavior is added via listeners, not by modifying every controller.
Liskov Substitution Clear contracts in include/ make substitution predictable.
Interface Segregation Small focused contracts prevent bloated services.
Dependency Inversion Business logic depends on contracts, not framework internals.
This isn’t “more architecture.”
It’s clearer boundaries.
---
Final Thought
A good Symfony project isn’t defined by how many patterns it uses.
It’s defined by:
- Clear structure
- Clean boundaries
- Predictable data flow
- Minimal cognitive friction
Clean architecture is not about layers.
It’s about protecting your core from chaos.
Originally published on LinkedIn ↗. The full text is hosted here.