Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Another Way to Structure your Symfony Project
Etienne Lebarillier
Etienne Lebarillier

Posted on

     

Another Way to Structure your Symfony Project

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.

Symfony default folder tree

 

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.

Symfony default folder tree opened

 

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 aDomain 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, theDomain folder depends on no one.

Inside theDomain folder, files should be grouped by domain purpose rather than technical purpose. This means noEntities,Services, orControllers folders here, only folder names that correspond to features or domain concepts.

Domain with modules inside opened

 

The Front Door of the Domain

The most important aspect of a project is what itdoes, 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 anApplication 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.

The Application folder opened with some queries and command inside

 

Connecting to the Outside World

With theApp andDomain 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 calledInfrastructure, which contains all the implementation details such as framework-specific code, database connections, and libraries.

Files in theInfrastructure folder depend on theApp andDomain files. For example, they might call an application handler from theApp folder or implement an interface defined in theDomain folder.

The infrastructure folder opened

Concretely, in Symfony, this involves modifying theController folder and declaring which services implement which interfaces.

# config/routes.yamlcontrollers:resource:path:../src/Catalog/Infrastructure/Controller/namespace:App\Catalog\Infrastructure\Controllertype:attribute
Enter fullscreen modeExit fullscreen mode

 

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; theUser 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

All the bounded contexts opened

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.

The different classes of project architectures, credits: Simon Brown

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

 

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

 

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

 

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(5)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
k0j0 profile image
Thierry Raoux
  • Joined

Thanks Etienne. This is one of the best introductory articles I've ever read about these architecture concepts. Absolutely clear, inspiring, enthusiastic. Hopefully I'll have a chance to implement this in a future project.

CollapseExpand
 
mauro_chojrin_611d4ede743 profile image
Mauro Chojrin
  • Joined

This looks really interesting! I wonder what changes to the configuration, if any, you need to make to have this working.

CollapseExpand
 
etienneleba profile image
Etienne Lebarillier
Software engineer | Crafts enthusiast | PHP/Symfony
  • Location
    France
  • Joined
• Edited on• Edited

Thank you for your comment !

I didn’t go into much detail about the configuration because it depends a lot on what you want to achieve. However, since Symfony 4.4, the framework has become very flexible. For a basic implementation, you just need to adjust the folder paths in theroutes.yaml andservices.yaml files.

That said, I encourage using a more refined dependency injection strategy to maintain better control. For instance:

  • In theApplication folder, allow onlyCommandHandler andQueryHandler classes to be injected.
  • Avoid dependency injection in theDomain folder entirely.
  • Enable full dependency injection in theInfrastructure folder.

This kind of setup takes less than 15 lines of configuration but provides a high degree of control.

I see two scenarios where the configuration might be a bit more complex:

  1. Using Symfony Messenger for commands, queries, and events:

    If you want to use Symfony Messenger as a bus, the documentation covers it here:

    Symfony Messenger: Multiple Buses

  2. Multiple adapters for domain interfaces:

    If your domain interfaces are implemented by multiple adapters (e.g., a real implementation and an in-memory one for tests), the Symfony docs provide everything you need:

    Symfony Service Container

I hope this helps!

CollapseExpand
 
mauro_chojrin_611d4ede743 profile image
Mauro Chojrin
  • Joined

Thanks!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software engineer | Crafts enthusiast | PHP/Symfony
  • Location
    France
  • Joined

More fromEtienne Lebarillier

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp