Skip to content

Screaming Architecture: When Structure Reveals Intent

Open a typical PHP project. You see Controllers/, Services/, Repositories/, Entities/. You know it uses Symfony or Laravel. You don't know if it manages a library, processes invoices, or tracks shipments. The structure tells you about the framework, not the business.

The Framework-Shaped Default

When you bootstrap a Symfony project, you get this:

src/
├── Controller/
├── Service/
├── Repository/
├── Entity/
└── Dto/

Every Symfony project looks the same from the outside, regardless of what it does. A developer joining the team has to read code, follow imports, and piece together which classes belong to which feature. The structure groups by technical role, not by purpose.

This works fine for small projects. When the codebase reaches 50+ entities, the Repository/ folder has files for users, books, invoices, and audit logs side by side. Finding related code means searching across four or five top-level directories.

Robert C. Martin coined the term "Screaming Architecture" to describe a different approach: a project where the folder structure tells you what the software does. A healthcare system's top-level directories should read "patients, appointments, prescriptions," not "controllers, models, views."

Business Modules First

Phexium organizes code by business module:

app/demo/src/
├── Homepage/
├── Library/
├── Loan/
├── Shared/
└── User/

A developer opening this directory knows what the application does: it manages a library with books, loans, and users. No code reading required.

Each module contains its own architectural layers:

Library/
├── Domain/
│   ├── Book.php
│   ├── BookRepository.php
│   ├── Author.php
│   ├── ISBN.php
│   ├── Title.php
│   └── Event/
├── Application/
│   ├── Command/
│   ├── Query/
│   └── EventListener/
├── Infrastructure/
└── Presentation/

The technical layers are still there. They are subordinate to the business grouping. You navigate by intent first ("I need the Library module"), then by responsibility ("I need the Domain layer").

What This Changes in Practice

Locality. When you work on book creation, everything you need is inside Library/. The entity, the value objects, the command, the handler, the repository interface, and the controller are all within the same module. You don't jump between src/Entity/Book.php, src/Service/BookService.php, and src/Controller/BookController.php in three separate directory trees.

Isolation. Modules don't import from each other's internals. Loan doesn't reach into Library/Domain/ to grab the Book entity directly. Cross-module communication happens through domain events or a shared module. This boundary is enforced by Deptrac (task deptrac:analyse), not by convention. If Loan accidentally imports from Library, the CI pipeline catches it.

Independent architecture per module. Not every module needs the same internal structure. In Phexium, Homepage uses the Direct Mode pattern: a simple UseCase class with no bus, no commands, no queries. Library and Loan use full CQRS with command and query buses. Each module owns its internals and can adopt the level of structure its complexity demands.

The Trap: Premature Decomposition

The risk with screaming architecture is over-splitting. If you create a module for every entity, you end up with dozens of tiny modules that fragment related behavior. A BookStatus module separate from Library makes navigation harder, not easier.

The heuristic: one module per business concept that has its own lifecycle and rules. If two entities always change together and share invariants, they belong in the same module. If they evolve independently and communicate through events, they are separate modules.

Phexium's Library module contains Book, Author, ISBN, Title, BookStatus, and BooksCollection, all in the same Domain directory. They form a cohesive aggregate. Splitting them would scatter related validation and business rules across directories for no benefit.

Trade-offs

More directories. A framework-default structure has five top-level folders. A module-based structure has one folder per business concept, each with its own internal hierarchy. IDE navigation matters more than tree depth, so invest in good search habits.

Upfront decisions. You need to identify your modules before writing code. Get it wrong, and you refactor directory structures later. But "get it wrong" means you misunderstood your domain, and that is a problem regardless of folder layout.

No framework scaffolding. make:controller doesn't know about your modules. You create files manually or build your own generators. Phexium accepts this trade-off because the structure serves the team, not the tooling.

What This Gets You

Look at your project's src/ directory. If someone who knows nothing about the codebase can guess what the application does from the folder names alone, your architecture screams its intent. If all they can tell is "it's a Symfony app," the structure is hiding the most important information behind a technical facade.

The goal is not to add layers or complexity. It is to make the first thing a developer sees match what the software actually does.