The MVC + Services architecture is so common in Symfony projects that it feels like the only way. It’s simple, familiar, and works... until it doesn’t. As your project grows, cracks begin to show: your business logic is everywhere, app behavior is unclear, and maintaining the code becomes painful. While it is the most common approach, Symfony does not force you to stick with it.
What if there was a better way ?
The Frustrations of Using the MVC + Services Architecture
Domain Logic is Spread Everywhere
As a project grows, business logic tends to spread across the entire code base. Every layer of the project — controllers, services, forms, entities — ends up containing bits and pieces of the domain model. This makes it increasingly difficult to focus on any specific part.
The Project Boundaries Are Not Clear
When your architecture is organized around technical layers, it becomes harder to identify clear boundaries between different contexts as the project grows. This lack of clarity can lead to tightly coupled code and maintenance challenges.
The Project Behaviors Are Not Clear
Since the default architecture emphasizes technical layers, it becomes quite challenging to understand the behaviors of the project. You might infer that certain entities are managed by specific services or guess the database schema, but the actual behaviors of the project, which are the most important aspects, remain unclear and implicit.
Different Lifecycles
When business logic is scattered across the project and mixed with implementation details, it becomes difficult to evolve them independently. Over time, The lifecycle of business logic tends to be much longer than that of implementation details (such as frameworks, third-party APIs, or databases). This mismatch forces you to rewrite large portions of code whenever even minor changes occur in a dependency.
A Better Way to Structure Your Architecture
Isolate the Business Logic
To make it easier to focus on the business logic when needed, the first step is to isolate it from the rest of the project. To achieve this, create a Domain
folder. This folder becomes the core of the project, where business logic is modeled using pure PHP objects, free from dependencies on implementation details. While the rest of the project depends on this folder, the Domain
folder depends on no one.
Inside the Domain
folder, files should be grouped by domain purpose rather than technical purpose. This means no Entities
, Services
, or Controllers
folders here, only folder names that correspond to features or domain concepts.
The Front Door of the Domain
The most important aspect of a project is what it does, the actions it can handle. These actions represent the project's behaviors and should serve as the only way to access the business logic. To reflect this, create an Application
folder that explicitly showcases all the project's behaviors. For example, as a new developer on the project, I should be able to understand at a glance what the project is capable of doing by looking at this folder.
Connecting to the Outside World
With the App
and Domain
folders, it becomes easy to focus on the business logic. However, at some point, this business logic needs to interact with external systems. To handle this, create a third folder called Infrastructure
, which contains all the implementation details such as framework-specific code, database connections, and libraries.
Files in the Infrastructure
folder depend on the App
and Domain
files. For example, they might call an application handler from the App
folder or implement an interface defined in the Domain
folder.
Concretely, in Symfony, this involves modifying the Controller
folder and declaring which services implement which interfaces.
# config/routes.yaml
controllers:
resource:
path: ../src/Catalog/Infrastructure/Controller/
namespace: App\Catalog\Infrastructure\Controller
type: attribute
Reveal the Boundaries
As your project evolves, you might notice that some parts of the business logic deserve their own space in the architecture. A good indicator is when the same term starts to have different meanings depending on the context. For example, the word Product might refer to a factory product, a warehouse product, or an e-commerce product, each requiring its own model. A god object can also be a good indicator; the User class often has this type of issue in Symfony projects.
When this happens, it’s time to extract it and let its business logic evolve independently.
Some contexts will form the core of your project, while others will support it. Generic contexts, such as Auth, can use a simpler architecture because they are not central to your domain
In this picture, we can see that the Auth context uses a standard Symfony structure, the Order and Catalog contexts use a domain-focused architecture, and the Shipping context uses a feature-focused architecture.
Incremental Modularity
If a specific context grows to the point where it needs to scale independently, consider splitting it into a separate deployment unit.
However, don’t rush into this step. Start by making your project modular, and if you notice that a context needs to scale individually, then deploy it separately.
Only split the codebase if organizational challenges arise, such as two teams struggling to collaborate on the same codebase.
Go Further
The Concepts
As we explored these solutions, we applied several craftsmanship concepts. Let’s name and briefly explain them, so you can dive deeper into each one.
Ubiquitous Language
Behind this unusual term lies a very simple concept. The ubiquitous language is the vocabulary your team uses to describe your domain model. This vocabulary should be documented and consistently used everywhere, in product conversations, the codebase, and beyond.
Concretely, create a markdown file at the root of a Bounded Context and bring together product people, domain experts, and tech teams to define each concept of your project.
Go Further
- Eric Evans - DDD - Chapter 2 : Communication and the use of language
- Martin Fowler - Ubiquitous Language
Bounded Context
A bounded context defines the linguistic boundary within your project, separating parts of the system where the ubiquitous language no longer aligns. Tools like Context Maps and Event Storming can help identify these boundaries.
A bounded context is an abstract concept; it can be implemented in many ways, from a simple folder in a modular monolith to a cluster in a microservices architecture.
Go Further
- Eric Evans - DDD - Part IV : Strategic Design
- Vaughn Vernon - IDDD - Chapter 2 : Domains, Subdomains, and Bounded Context
- Martin Fowler - Bounded Context
Ports and Adapters, Hexagonal, Onion, and Clean Architecture
All these architectures aim to isolate business logic from implementation details. Whether you use Ports and Adapters, Hexagonal, or Clean Architecture, the core idea is to make the business logic framework-agnostic and easy to test.
Once you have this in mind, there is a full spectrum of implementations, and the best one depends on your context and preferences. A major advantage of this architecture is that, by isolating your business logic, it enables much more efficient testing.
Go Further
- Alistair Cockburn - Hexagonal Architecture
- Uncle Bob - Clean Architecture
- Herberto Graca - DDD, Hexagonal, Onion, Clean, CQRS, … How I Put It All Together
Screaming Architecture
The idea of organizing folders and files to "scream" the business logic is known as Screaming Architecture. This concept emphasizes that the structure of your code should make the project’s purpose immediately clear. The goal is for a new developer to understand what the project does at a glance.
I highly recommend reading Uncle Bob’s article on the topic—his comparison to a house plan is particularly insightful.
Go Further
Vertical Slicing Architecture
Vertical slicing organizes your project by features, allowing each feature to evolve independently. It enables you to apply different architectures to different features based on complexity and maturity.
Although the idea is interesting, it requires highly skilled engineers to implement and maintain such an architecture effectively.
Go Further
Final Thoughts
The way you structure your Symfony project has a profound impact on its scalability, maintainability, and clarity. By isolating your business logic and making behaviors explicit, you’ll create a system that’s easier to understand and evolve.
If you’re new to these ideas, don’t worry, software craftsmanship is a journey, not a destination. The concepts might seem overwhelming at first, but each one will help you deliver more value to your business.
Have questions or want to share your experience ? Drop them in the comments ! And stay tuned for the next article 🚀
Top comments (0)