The 18-month refactor
The app works, it grows, and each new feature costs more than the last. The moment you should accelerate is the moment you rewrite.
App architecture — the layou-dev-kit
An architecture held up by good intentions doesn't hold. This one is split into modules whose dependencies are checked by the compiler. If presentation imports data, it isn't a warning, a lint, or a review comment: it's a red error that blocks the build.
The 5 modules, and what each is allowed to see
The cost of no architecture
The app works, it grows, and each new feature costs more than the last. The moment you should accelerate is the moment you rewrite.
One person knows why that file exists. The knowledge lives in a head, not in the structure.
You debate indentation and naming while a database call slips into a component.
The foundations
Everyone quotes these principles. Knowing them was never the question: the question is what holds them up on a Tuesday night, on a late feature, when nobody has time to review. For each one, here's the mechanism that enforces it without relying on the team's good will.
One file, one reason to change.
One public class per file, 50 lines max per function, 200 per class. Beyond that the lint breaks the build: nobody argues it in review, you split.
Open for extension, closed for modification.
A second implementation lands next to the first — SqlUserRepository beside RestUserRepository — with no renaming and no caller touched. The name carries the tech, not the fact of implementing.
One implementation must replace another without surprises.
Every interface has its Fake…, injected in place of the real one in tests. A single test prefix is allowed: no Mock, Stub and InMemory side by side suggesting three different contracts.
One contract per business role, not one catch-all.
One interface per feature — AuthRepository, OrderRepository — declared in domain. And reads don't cross a use case: you don't build a layer for the pleasure of symmetry.
The business depends on nothing. Everything else depends on the business.
That's the matrix below. domain declares the interface and imports nobody, data implements it, app is the only place the two ever meet. A backwards arrow doesn't compile.
The simplest solution that holds.
80% of services are five lines: a class, an injection. The full ceremony — interface, fake, typed failures — is reserved for the 20% where it pays: payments, auth, anything tested in isolation.
You don't write for a need you don't have.
An interface isn't extracted “just in case”: it's extracted the day a test needs a fake. Pre-abstracting is taking on debt for a future that never shows up.
Each layer has a single subject.
A leaf component knows only its parameters: zero access to the state manager, checked by the lint. A single point of the UI tree wires state in — anywhere else is a leak.
You assemble small pieces, you don't stack classes.
Several small pieces of state watching each other, rather than an 800-line god object. Same rule on the UI side: a component containing another component is an organism, not a leaf.
One principle is deliberately missing from this list: DRY. It stops at the data boundary, where the DTO duplicates the entity's shape — on purpose. That duplicate is what makes a renamed database field break one mapper, instead of fifteen screens.
What it returns
An architecture promise with no mechanism is a slogan. For each one, here's what actually enforces it.
Add people without slowing the team down
The monorepo is split by layer, not by screen. Two devs on two features don't collide: they don't touch the same modules.
The blast radius of a change is known upfront
When a business rule moves, it moves inside domain. Nothing else compiles differently. That's what a held architecture buys: knowing what you're not breaking.
domain is pure: testable with no emulator, no database, no UI componentdataThe MVP and the V3 are the same codebase
The layers are laid down from day 1, even for an MVP. No throwaway code, so no rewrite exactly when the app finally starts working.
Either / Result): the error path lives in the signature, not in a forgotten try/catchFew chokepoints, so little to audit
An architecture doesn't secure an app — it decides how many places you must go through. That number is what makes an audit feasible.
.env. Client-side, they're injected at compile timedata boundaryThe rule
Each module declares its dependencies. This table isn't a team convention pinned to a wall: it's the real state of the manifests. A red cell is a broken build.
| Module… | core | domain | data | presentation | app |
|---|---|---|---|---|---|
| core | · | ✕ | ✕ | ✕ | ✕ |
| domain | ✓ | · | ✕ | ✕ | ✕ |
| data | ✓ | ✓ | · | ✕ | ✕ |
| presentation | ✓ | ✓ | ✕ | · | ✕ |
| app | ✓ | ✓ | ✓ | ✓ | · |
// ✅ OK — presentation → domain
import app/domain/auth/entities/app_user
// ❌ ERREUR DE COMPILATION — data qui fuite dans presentation
import app/data/auth/datasources/auth_remote_datasource Tooling
What the compiler can't see goes through custom lint rules. These are rules nobody has to defend in review — they fail on their own.
And there's no escape hatch: disabling a rule with an ignore comment is forbidden by the kit. You refactor instead.
Before / after
| Without the dev-kit | With | |
|---|---|---|
| Forbidden import | It compiles (but it's broken) | Compilation error |
| Oversized component | Nobody notices | Lint error at 200 lines |
| Leaf component wired to state | It works | Lint error |
| Testing a component | Mock hell | Zero mocks |
| New AI agent | Must re-read every convention | The compiler guides it |
Why it matters in 2026
An AI agent writes fast, and it cheats: a datasource imported into a component, three classes in one file, local state “because it's simpler.” The worst part is that it compiles — and quietly rots the codebase. What stops it isn't a longer prompt: it's a project where the shortcut doesn't compile. The agent gets the build error, understands the constraint, and corrects itself with no human in the loop. The compiler is the reviewer that never sleeps and never loses its context.
I don't need to trust the AI. The architecture itself is the guardrail.See the full delivery loop →
Honestly
Budget 2-3 days of monorepo and module scaffolding on the first project, plus about a day to write the lint rules with their tests. After that it carries over from project to project.
A hackathon, a prototype you bin in two weeks: the 4 layers cost more than they return. It pays off when the code has to outlive its author.
The dev-kit contains no database access rules, no encryption policy, no GDPR compliance. It reduces the number of chokepoints — the audit and the rules remain work in their own right.
In detail
The same 9 views — modules, layers, data flow, atomic design, injection, errors, naming, UI, services — across three stacks: Flutter/Firebase, React Native/Supabase, TanStack/Convex. The tabs at the top switch stacks, the architecture doesn't change. Keyboard navigation (1-9, Esc), or Auto Tour. The explorer's interface is in French.
Open fullscreen ↗Existing codebase or from-scratch start: we look at what holding this architecture at your place would cost, and what it would spare you.