Flat Layered Architecture
On this page
Flat Layered Architecture is the shape I give to a Rust service when it has to exist quickly: one folder per kind of code, all at a single level, and a request that always travels the same short path from route to controller to service to repository. It is my own convention, not a design from a book, though it resembles patterns that others have named before me.
It is one of the architectures I use, and for me it is a stage rather than a destination. A module often starts flat so that it can work as soon as possible; when it grows and has to be maintained for years, I refactor it into Clean Architecture.
At a glance
| Origin | My own convention; close to a classic layered architecture with transaction scripts |
|---|---|
| I use it for | First versions of Rust services that have to work quickly, and work done with a local AI agent that has a small context window |
| I do not use it for | Modules that have grown and will be maintained for years |
| Languages | Rust today; earlier also Node.js and Python, which I have since migrated to Clean Architecture |
The idea in brief
A. One folder per kind, one level deep
The source tree has one folder for each kind of code — routes, controllers, services, repositories, entities, validations, middlewares, infrastructure, utilities — and they all sit directly under the source root. Nothing nests deeper except where one kind genuinely has variants, such as repositories for a particular database.
B. One short path for every request
A request always travels the same way: the route maps a path to a controller, the controller validates the input and calls a service, and the service calls the repositories it needs. Nothing calls backwards along that path.
C. One file per operation
Each service file holds one operation of the system — adding a user, generating a report — exposed as a single function named execute. The name of the file is the name of the operation, so the list of files in the services folder is the list of things the system can do.
D. Concrete calls, no ports
A service calls repository functions directly, with no interface in between. That is the main thing this architecture gives up compared with Clean Architecture: the business logic knows which database it talks to. In exchange, every call can be followed by jumping straight to its definition.
Why I chose it
Sometimes I have to build quickly. Clean Architecture is the better long-term structure, but it asks for more before the first request is answered: more folders, more interfaces, more wiring. When a service has to work soon, a flat structure lets me spend that time on the operations themselves.
Fewer module files and shallower relations. In the module style I use in Rust, every folder needs its own mod.rs file to declare what it contains, and a deep tree of layers and sub-layers means many of them, with relations that branch in several directions. One level of folders keeps those files few and short, and keeps every relation one step long.
It fits in the context window of a local AI agent. When I cannot use an online AI agent and have to work with one that runs on my own machine, its context window — the amount of text it can hold at once — is small. With Clean Architecture, much of that window is spent just describing the structure. With a flat structure, the agent can see the whole shape of the service and still have room for the code it is changing.
It is easy to refactor later. Because each operation already lives in its own file, each one is already close to what Clean Architecture calls a use case. Moving to Clean Architecture is mostly a matter of moving files and putting an interface between the operation and the database, not rethinking the logic.
My conventions
A Rust service of mine that follows this architecture has this tree:
A service is a single operation, and it calls the repository functions it needs directly:
// services/add_item.rs
pub async | Folder | May call | Must not do |
|---|---|---|
routes/ | controllers | contain logic |
controllers/ | validations, services | touch the database |
services/ | repositories, entities, utilities | know about HTTP |
repositories/ | the database | contain business decisions |
Two optional folders appear when a service needs them: queues/ and workers/ for background jobs, and ffi/ when the service calls a library written in C.
How I check a module against it
These are the questions I ask a flat module, taken from the conventions above:
| Area | Question I ask | Source |
|---|---|---|
| Depth | Is every folder directly under the source root, with nesting only for real variants? | one level deep |
| Operations | Is every operation its own service file with a single execute function? | one file per operation |
| Direction | Does every call go from route to controller to service to repository, never back? | the call table |
| Controllers | Do controllers stay out of the database? | the call table |
| Routes | Do routes only map paths to controllers? | the call table |
| Readiness to grow | Could each service move to a use case without rewriting its logic? | the refactor path |
Where I do not use it
When a module grows and has to last
Once a module holds many operations, is shared with other systems, or has to be maintained for years, the shortcuts stop paying off. Business logic that knows its database becomes harder to test and harder to change, and that is when I refactor it into Clean Architecture. Because the flat structure was built with this move in mind, the folders map onto the new ones directly:
| Flat folder | Becomes in Clean Architecture |
|---|---|
services/ | application/use_cases/, grouped by resource |
repositories/ | infrastructure/persistence/, behind ports declared in application/ports/ |
controllers/, routes/, middlewares/ | interface/http/ |
entities/ | domain/ |
validations/ | application/validation/ |
utils/ | shared/ |
infrastructures/ | infrastructure/ |
During the move, the flat version and the refactored version sit side by side in the repository, so the module always has a working version.
References
This architecture is my own convention and has no book behind it. Its closest named relatives are described by Martin Fowler in Patterns of Enterprise Application Architecture (Addison-Wesley, 2002): the Transaction Script, where each operation is one procedure that does all its own work, and the Service Layer, which gathers those operations behind one boundary. I mention them as resemblances, not as the source.
Terms
- Layer
- A group of code with one kind of responsibility, such as handling requests or reaching the database.
- Transaction script
- A way of organising logic in which each operation of the system is one procedure that does all its own work from start to finish.
- Repository
- Code that reads and writes one kind of data in the database, so that the rest of the program does not write queries itself.
mod.rs- The file in a Rust folder that declares which modules the folder contains. In the module style I use, every folder of code has one.
- Context window
- The amount of text an AI model can hold and work with at once. Local models usually have much smaller ones than online models.
Technologies covered
- Rust