App architecture — the layou-dev-kit

A bad import doesn't compile.

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

What breaks an app is almost never the tech

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.

The dev who can't leave

One person knows why that file exists. The knowledge lives in a head, not in the structure.

The review that argues style

You debate indentation and naming while a database call slips into a component.

The foundations

SOLID, KISS, YAGNI — and what enforces them

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.

Single Responsibility

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 / Closed

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.

Liskov Substitution

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.

Interface Segregation

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.

Dependency Inversion

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.

Keep It Simple

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 Aren't Gonna Need It

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.

Separation of Concerns

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.

Composition over inheritance

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

Four benefits, and the mechanism behind each

An architecture promise with no mechanism is a slogan. For each one, here's what actually enforces it.

Scalability

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.

  • A feature is a vertical slice across 4 separate modules
  • Module splitting keeps builds incremental — you don't recompile the whole app
  • A new dev reads 5 folders, not 300 files — the structure tells them where their code goes
  • Naming conventions are fixed: the file to create is deduced, not debated

Maintainability

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 component
  • Repos are interfaces — swap in a fake with one line of injection
  • Switching databases only touches data
  • Max 50 lines per function, 200 per class, 1 public class per file

Evolvability

The 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.

  • Errors are typed: a closed failure type + exhaustive matching — adding a failure case becomes a compile error everywhere it's missing
  • The return type carries the failure (Either / Result): the error path lives in the signature, not in a forgotten try/catch
  • Every external SDK is wrapped before it reaches app code
  • Migrations happen layer by layer, not in a big bang

Security

Few 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.

  • The client doesn't open an endpoint per need. It writes an intent to the database, a server trigger processes it. One entry surface to govern instead of a fleet of endpoints
  • Every write goes through a mandatory use case — one place to validate, not twelve write paths
  • Server secrets live in a secret manager, never in a production .env. Client-side, they're injected at compile time
  • No raw SDK exception reaches the UI: it's translated into a typed business failure at the data boundary

The rule

What the compiler refuses

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.

What the compiler refuses
Module… core domain data presentation app
core ·
domain ·
data ·
presentation ·
app ·
  • can import
  • won't compile
// ✅ 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

Discipline, delegated to the machine

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.

  • A leaf component never touches the state manager error
    A leaf component receives everything through its parameters. That's what makes it testable with zero mocks.
  • A single wiring point between state and the UI tree error
    The connector is the only place state touches the component tree. Anywhere else is a leak.
  • 50 lines max per function warning
    A longer function hides a decision nobody named.
  • 200 lines max per class warning
    Beyond that, the class does two things. Split it before it hardens.
  • 1 public class per file warning
    The file path becomes the project index — you find without searching.

And there's no escape hatch: disabling a rule with an ignore comment is forbidden by the kit. You refactor instead.

Before / after

The same shortcut, in both worlds

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

A constraining architecture is one an agent can follow

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

What it costs, and what it doesn't do

It isn't free

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.

On a throwaway MVP, it's overkill

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.

It isn't a security framework

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 interactive explorer

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 ↗

Your app is starting to weigh

Existing codebase or from-scratch start: we look at what holding this architecture at your place would cost, and what it would spare you.