Skip to content
Architecture Programming

Flat Layered Architecture

By Romi Fadli, S.Kom. 7 min read Software Design
On this page
Four flat layers stacked on the left — routes, controllers, services, repositories — joined by short lines, with an orange line labelled later: refactor leading to the concentric circles of Clean Architecture on the right

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

OriginMy own convention; close to a classic layered architecture with transaction scripts
I use it forFirst 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 forModules that have grown and will be maintained for years
LanguagesRust today; earlier also Node.js and Python, which I have since migrated to Clean Architecture
Where Flat Layered Architecture sits among the architectures I use.

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:

bash
src/
├── routes/           # path → controller, nothing else
├── controllers/      # read the request, validate, call one service, build the response
├── services/         # one file per operation, each exposing execute()
├── repositories/     # database access, grouped by database: postgresql/
├── entities/         # data shapes shared by services and repositories
├── validations/      # input rules, one file per resource
├── middlewares/      # cross-cutting request handling: auth, logging, headers
├── infrastructures/  # database pool, HTTP server, telemetry, configuration
├── utils/            # small helpers with no business meaning
└── main.rs           # wires the server, the routes, and the middlewares

A service is a single operation, and it calls the repository functions it needs directly:

rust
// services/add_item.rs
pub async fn execute(db: &DatabaseConnection, name: String) -> Result<String, String> {
    let item = ItemEntity { id: None, name };
    item_postgres_repository::create(db.get_pool(), &item)
        .await
        .map_err(|e| format!("Failed to create item: {}", e))
}
FolderMay callMust not do
routes/controllerscontain logic
controllers/validations, servicestouch the database
services/repositories, entities, utilitiesknow about HTTP
repositories/the databasecontain business decisions
The direction of calls between the main folders.

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:

AreaQuestion I askSource
DepthIs every folder directly under the source root, with nesting only for real variants?one level deep
OperationsIs every operation its own service file with a single execute function?one file per operation
DirectionDoes every call go from route to controller to service to repository, never back?the call table
ControllersDo controllers stay out of the database?the call table
RoutesDo routes only map paths to controllers?the call table
Readiness to growCould each service move to a use case without rewriting its logic?the refactor path
The questions I check a flat module against.

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 folderBecomes 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/
How each flat folder moves when a module is refactored.

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

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

Three identical empty application nodes stacked as copies, connected to a database, a cache, object storage, a pool of queue workers, and a telemetry collector that hold all the state

How I run Laravel as the core of a team-built platform: nodes that hold no state, slow work handed to queued jobs and specialised services, files kept off the server, one internal token for every login, and every request, job, and call observable.

13 min read Read more →
One endpoint procedure as a column of ordered steps, a request entering at the top and the answer leaving at the bottom, each step calling its own concern module beside it

Each endpoint is one procedure that carries a request from start to finish, with its concerns in modules beside it — Fowler's Transaction Script as I shape small one-job Python services, bundled into one binary, until their rules outgrow it.

8 min read Read more →
PDF pages of different sizes passing through a renderer that reads from and writes back to storage, producing thumbnails of one uniform size

A stateless Rust service that renders the first page of a PDF into a sharp, uniformly sized WebP thumbnail, reading the PDF and writing the image through two presigned links, so no application streams files or holds storage credentials.

8 min read Read more →
A document's fingerprint passing through a clock, signed by a certificate below it, and returning as a time seal stamped on the same document

A Rust timestamp authority that stamps a document's hash with a signed, NTP-backed time per RFC 3161, so PDF signatures stay verifiable after their certificates expire — issuing and renewing its own signing certificate, with public OCSP and CRL.

9 min read Read more →
A chain of three certificates, root to issuing CA to signer, signing a PDF, while an OCSP answer and a revocation list from the issuing CA reach a validator shield alongside the document

An issuing certificate authority in Rust that gives each PDF signer a certificate of their own, then answers every reader's question — is it still valid? — through a public OCSP responder and revocation list, and hands the signing service each key and chain as PKCS#12.

11 min read Read more →
Four concentric circles with arrows coming in from a database, a browser window, and a message queue, all pointing toward the centre.

Clean Architecture

Programming

Clean Architecture is one of the architectures I use: why I chose it, the folder and naming conventions every module of mine follows across Rust, Python, Dart, Node.js and C, the questions I check a module against, and where I use something else instead.

12 min read Read more →

Related Articles

A panel of code lines, some starting with highlighted names, connected by three curves to a person, a clock, and a chat window whose older history is faded above a dashed line.

My code is read by people who were not there when it was written: colleagues, me months later, or an AI agent that forgets on every reset. The clean code rules from Robert C. Martin I apply in every language, shown in a Rust service.

24 min read Read more →