Practical guide
How to design the architecture of a larger PHP application
Do not start with a diagram for the whole world. Choose one important process, name its rules, and build layers around it afterwards.
First, the short version
Architecture is mostly an agreement
Architecture says where rules, data, and technical details belong. Good architecture does not make an application “enterprise”. It lets people change one thing without fearing ten side effects.
DDD helps you start with the language of the business. The domain is where the company’s rules live: for example, when an order can be sent or an invoice approved.
Get ready
What you need before drawing folders
Understand the problem first. Folders and class names come afterwards.
- One concrete flow, such as creating an order, approving a quote, or changing a complaint status.
- Someone who knows the business rules and can confirm what should happen in both normal and failing situations.
- A PHP project where you can make small incremental changes. You do not need to restructure all of src/ on day one.
- A place for short notes. One page of terms, rules, and examples is enough at first.
Steps 1 to 3
Put boundaries around real work
A layer is not a folder for the sake of a folder. It needs a clear role and a clear idea of what must not cross it.
1. Name the domain in human language
- Write three to five sentences that explain one process without technical words. For example: “an order can be sent only after successful payment”.
- List important nouns and verbs. They become names for classes, methods, and use cases.
- When two people understand one word differently, distinguish it early. An unclear name in code costs more than one extra class.
2. Split responsibilities into layers
- The domain holds rules and business terms. It should not know about HTTP, SQL, or a particular database.
- The application layer composes a use case: it loads data, calls domain rules, and saves the result. A CreateOrder class is a good fit here.
- Infrastructure handles technology: a database, email, a file, or an external API. It implements interfaces the application needs.
- An entry layer, such as a Symfony controller or CLI command, only translates input into a use case and returns a response.
mkdir -p src/Domain src/Application src/Infrastructure Official Symfony documentation for services and their wiring 3. Protect the dependency direction
- Inner layers must not import concrete Symfony, Doctrine, or SDK classes. Otherwise rules quickly become technical details.
- When the application needs to save an order, it depends on an OrderRepository interface. The database implementation belongs outside, in infrastructure.
- Write every new dependency direction briefly in the README. Architecture then will not be just a diagram from last year.
php bin/console debug:container --types Official Symfony documentation for typed dependencies Step 4
Check boundaries on one use case
The best architecture test is a small change. Try making it without touching everything around it.
-
Find the domain rule
Open the use case and point to the class or method where its main rule actually lives. If it is hidden in a controller or SQL query, move it closer to the domain.
-
Replace a technical detail in a test
The use case should be testable with a simple in-memory repository substitute. It should not need a database connection or web server.
php bin/phpunit -
Check layer dependencies
Look for framework imports in the domain. An individual occurrence may have a reason, but it should be a deliberate exception rather than the default.
rg "Symfony\\|Doctrine\\" src/Domain
If something goes wrong
Common problems
You are rebuilding the whole project at once
Choose one use case, surround it with tests, and move it first. A large “architecture sprint” often only stops feature delivery and proves nothing.
Empty layers full of forwarding methods appear
A layer should exist because of a rule or boundary, not because of a name from an article. If it has no responsibility yet, do not create it yet.
The domain calls a database or external API directly
Add a small interface in the application or domain part and put the concrete client in infrastructure. Rules can then be tested without the network.
The team uses different names for the same thing
Keep a short glossary beside the module and reflect it in class names. Consistency is more valuable in a large application than a perfectly clever name.
Done
You know where rules and technology belong.
DDD can now be a compass, not a mandatory template. Add boundaries gradually where the application truly grows.