Architecture d'app · le layou-dev-kit

Un mauvais import ne compile pas.

Les apps qu'on livre sont découpées en modules dont les dépendances sont vérifiées par le compilateur. Si presentation importe data, le build s'arrête en rouge. Personne n'a besoin de s'en souvenir en revue de code.

Les 5 modules, et ce que chacun a le droit de voir

Le coût du sans-archi

Ce qui casse une app, c'est rarement la techno.

  • Le refactor à 18 mois. L'app grossit, chaque fonctionnalité coûte plus cher que la précédente, et on réécrit au moment où il faudrait accélérer.
  • Le dev qui ne peut pas partir. Une seule personne sait pourquoi ce fichier est là. La connaissance vit dans sa tête, nulle part dans la structure.

La règle

Ce que le compilateur refuse

Chaque module déclare ses dépendances dans son manifeste, et ce tableau en est l'état réel. Une case rouge, c'est un build cassé.

Ce que le compilateur refuse
Le module… core domain data presentation app
core ne compile pas ne compile pas ne compile pas ne compile pas
domain peut importer ne compile pas ne compile pas ne compile pas
data peut importer peut importer ne compile pas ne compile pas
presentation peut importer peut importer ne compile pas ne compile pas
app peut importer peut importer peut importer peut importer
  • peut importer
  • ne compile pas
// ✅ 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

L'outillage

Le même raccourci, avec et sans le kit

Ce que le compilateur ne voit pas passe par des règles de lint maison. Elles échouent toutes seules, et le kit interdit de les désactiver avec un commentaire ignore : on refactorise.

Le même raccourci, avec et sans le kit Sans le kit Avec
Import interdit Ça compile, et c'est cassé Erreur de compilation
Composant trop gros Personne ne le voit Erreur de lint à 200 lignes
Composant feuille branché sur l'état Ça marche Erreur de lint
Test d'un composant Des mocks partout Zéro mock
Les 5 règles de lint et leur sévérité
  • Un composant feuille ne touche pas le gestionnaire d'état erreur
    Il reçoit tout par ses paramètres, ce qui le rend testable sans un seul mock.
  • Un seul point de branchement entre l'état et l'arbre d'UI erreur
    Le connecteur est le seul endroit où l'état touche les composants. Ailleurs, c'est une fuite.
  • 50 lignes max par fonction avertissement
    Une fonction plus longue cache une décision qu'on n'a pas nommée.
  • 200 lignes max par classe avertissement
    Au-delà, la classe fait deux choses. On la coupe avant qu'elle durcisse.
  • 1 classe publique par fichier avertissement
    Le chemin du fichier devient l'index du projet : on trouve sans chercher.

Ce que ça change pour votre app

Quatre effets, chacun avec son mécanisme

Ajouter du monde sans ralentir

Le monorepo est découpé par couche. Deux développeurs sur deux fonctionnalités ne touchent pas les mêmes modules.

Le mécanisme
  • Une fonctionnalité est une tranche verticale dans 4 modules séparés
  • Le découpage en modules rend le build incrémental : on ne recompile pas l'app entière
  • Un nouveau développeur lit 5 dossiers, et la structure lui dit où va son code
  • Les conventions de nommage sont fixes, le fichier à créer se déduit

Savoir d'avance ce qu'un changement touche

Quand une règle métier bouge, elle bouge dans domain, et rien d'autre ne compile différemment.

Le mécanisme
  • domain se teste sans émulateur, sans base de données, sans composant d'UI
  • Les repos sont des interfaces : on branche un fake en une ligne d'injection
  • Changer de base de données ne touche que data

Le MVP et la V3 sont le même code

Les couches sont posées dès le premier jour, même pour un MVP. Il n'y a pas de code jetable à réécrire au moment où l'app commence à marcher.

Le mécanisme
  • Les erreurs sont typées (type fermé et filtrage exhaustif) : ajouter un cas d'échec devient une erreur de compilation partout où il manque
  • Le type de retour porte l'échec (Either / Result), le chemin d'erreur est écrit dans la signature
  • Tout SDK externe est encapsulé avant d'atteindre le code de l'app
  • Les migrations se font couche par couche

Peu de points de passage à auditer

L'architecture fixe le nombre d'endroits par lesquels une donnée entre. Moins il y en a, plus un audit est faisable.

Le mécanisme
  • Le client écrit une intention en base et un déclencheur serveur la traite : une seule surface d'entrée à gouverner
  • Toute écriture passe par un cas d'usage obligatoire, le seul endroit où valider
  • Les secrets serveur vivent dans un gestionnaire de secrets, jamais dans un .env de production. Côté client, ils sont injectés à la compilation
  • Une exception brute du SDK est traduite en erreur métier typée à la frontière data, avant d'atteindre l'UI

Les fondations

SOLID, KISS, YAGNI, et ce qui les fait tenir

Pour chaque principe, le mécanisme du kit qui l'applique un mardi soir, sur une fonctionnalité en retard, quand personne n'a le temps de relire.

Single Responsibility Un fichier, une raison de changer.

Une seule classe publique par fichier, 50 lignes max par fonction, 200 par classe. Au-delà, le lint casse le build et on découpe.

Open / Closed Ouvert à l'extension, fermé à la modification.

Une seconde implémentation s'ajoute à côté de la première (SqlUserRepository à côté de RestUserRepository) sans renommer ni toucher un seul appelant. Le nom porte la techno utilisée.

Liskov Substitution Une implémentation en remplace une autre sans surprise.

Chaque interface a son Fake…, injecté à la place de la vraie en test. Un seul préfixe de test est autorisé : voir Mock, Stub et InMemory côte à côte ferait croire à trois contrats différents.

Interface Segregation Un contrat par rôle métier.

Une interface par fonctionnalité (AuthRepository, OrderRepository), déclarée dans domain. Les lectures ne traversent pas de cas d'usage : on n'ajoute pas une couche pour la symétrie.

Dependency Inversion Le métier ne dépend de rien. Tout le reste dépend du métier.

C'est la matrice plus haut. domain déclare l'interface et n'importe personne, data l'implémente, app est le seul endroit où les deux se rencontrent. Une flèche à l'envers ne compile pas.

Keep It Simple La solution la plus simple qui tient.

80 % des services font cinq lignes : une classe, une injection. L'interface, le fake et les erreurs typées sont réservés aux 20 % où ils paient : paiement, authentification, ce qui se teste en isolation.

You Aren't Gonna Need It On n'écrit pas pour un besoin qu'on n'a pas.

Une interface s'extrait le jour où un test a besoin d'un fake. Pré-abstraire « au cas où », c'est prendre de la dette pour un futur qui n'arrive pas.

Separation of Concerns Chaque couche a un seul sujet.

Un composant feuille ne connaît que ses paramètres, et le lint vérifie qu'il n'accède jamais au gestionnaire d'état. Un seul point de l'arbre d'UI branche l'état.

Composition > héritage On assemble des petites pièces.

Plusieurs petits états qui s'observent plutôt qu'un objet-dieu de 800 lignes. Côté UI, un composant qui en contient un autre est un organisme.

DRY manque volontairement à cette liste. Il s'arrête à la frontière data, où le DTO duplique exprès la forme de l'entité : un champ renommé côté base casse un seul mapper, aucun écran.

Les limites

Ce que ça coûte, et où ça s'arrête

  • Deux à trois jours au départ. Le scaffolding du monorepo et des modules prend 2 à 3 jours sur le premier projet, plus environ une journée pour écrire les règles de lint et leurs tests. Ensuite, tout se réutilise d'un projet à l'autre.
  • Trop lourd pour un prototype jetable. Pour un hackathon ou un prototype jeté dans deux semaines, les 4 couches coûtent plus qu'elles ne rapportent. Elles deviennent rentables quand le code doit survivre à son auteur.
  • La sécurité reste un chantier à part. Le kit ne contient ni règles d'accès à la base, ni politique de chiffrement, ni conformité RGPD. Il réduit les points de passage, l'audit reste à faire.

En détail

L'explorateur interactif

Neuf vues de la même architecture (modules, couches, flux de données, injection, erreurs, nommage…), déclinées sur Flutter/Firebase, React Native/Supabase et TanStack/Convex. Les onglets du haut changent de stack. Au clavier : 1 à 9 et Échap, ou Auto Tour.

Ouvrir en plein écran ↗

Votre app commence à peser ?

Reprise d'un existant ou départ de zéro : on regarde ce que coûterait cette architecture chez vous, et ce qu'elle vous éviterait.

Parler de mon application