🏗️ Comment j’organise mes solutions .NET pour des projets scalables et faciles à maintenir

J’ai eu l’occasion de travailler sur des projets très différents : applications web complexes, APIs métiers, plateformes 3D, outils internes, projets mobiles…
Une chose revient toujours : la structure d’un projet conditionne sa qualité et sa durée de vie.

Une mauvaise structure = un projet fragile.
Une bonne structure = un projet qui respire, évolue et reste propre.

Dans cet article, je partage comment j’organise mes solutions .NET, de manière simple et pragmatique, pour obtenir un code :

✔ propre
✔ testable
✔ évolutif
✔ scalable
✔ agréable à maintenir


🔹 1. Séparer clairement le domaine, l’application, l’infrastructure et le web

J’utilise souvent une organisation inspirée de Clean Architecture, mais sans aller dans l’excès.
Le but est simple : chaque couche doit avoir une responsabilité.

Voici la structure que j’utilise le plus souvent :

src/
 ├─ Project.Domain
 ├─ Project.Application
 ├─ Project.Infrastructure
 └─ Project.Api

Project.Domain

➡️ Le cœur métier
➡️ Entités, value objects, règles métier
➡️ Pas de dépendance technique

Project.Application

➡️ Les cas d’usage
➡️ Services applicatifs
➡️ Interfaces (Ports) vers l’infra
➡️ DTOs, validations, handlers
➡️ Utilisation légère de MediatR selon les besoins

Project.Infrastructure

➡️ Implémentation des interfaces
➡️ EF Core, SQL Server, Redis, Files, API externes
➡️ Repositories, configuration, migrations

Project.Api

➡️ Exposition HTTP
➡️ JWT / Policies / Rate Limiting
➡️ Contrôleurs, minimal APIs, endpoints
➡️ Mapping entre Application et API

Chacune a son rôle, aucune ne dépasse.
C’est la base d’une architecture saine.


🔹 2. Tout isoler derrière des Interfaces — surtout ce qui touche à l’extérieur

Base de la maintenabilité :
👉 Je ne travaille jamais directement avec EF, Redis, ou un service externe dans la couche Application.

Je crée systématiquement des interfaces :

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

Puis l’implémentation se trouve en Infrastructure :

public class SmtpEmailSender : IEmailSender { ... }

Avantages :

  • testabilité (mock facile)
  • découplage total
  • remplacement rapide d’une technologie

🔹 3. Toujours centraliser la configuration

Je n’éparpille jamais le code de config.
Tout est regroupé dans un dossier Configuration ou Options :

Project.Api/
 └─ Config/
      ├─ JwtOptions.cs
      ├─ CorsOptions.cs
      ├─ DatabaseOptions.cs
      └─ RateLimitOptions.cs

Ensuite, j’utilise IOptions<T> proprement.

✔️ C’est plus clair
✔️ Ça évite la magie
✔️ Ça rend la maintenance très simple


🔹 4. Une solution doit rester lisible : pas plus de 15–20 projets

Un piège fréquent (surtout dans les grosses équipes) :
💥 multiplier les projets sans raison

Je garde toujours une convention simple :
➡️ Tant que le projet fait moins de 500 fichiers → 4 projets (Domain, App, Infra, API) suffisent.

On n’ajoute des projets supplémentaires que si nécessaire :

  • un module séparé ?
  • un adapter externe ?
  • un background worker ?

Mais jamais juste “pour faire Clean Architecture comme dans un tuto”.


🔹 5. Un dossier “Shared” propre pour les cross-cutting concerns

Les éléments transverses (log, date providers, exceptions, result pattern…) vont dans un dossier partagé :

Project.Domain/
 └─ Shared/
      ├─ Result.cs
      ├─ DomainException.cs
      └─ TimeProvider.cs

Je fais très attention à ne pas en abuser.
Un “Shared” trop gros = dette technique.


🔹 6. Prévoir la testabilité dès le début

Je crée toujours le projet de tests au même moment que la solution :

tests/
 └─ Project.Tests

Puis je m’assure que tout est testable par construction :

  • pas de logique dans les contrôleurs
  • dépendances injectées
  • classes petites
  • méthodes pures quand c’est possible
  • validation dans l’Application, pas dans l’API
  • mocks faciles via interfaces

Ce principe m’a beaucoup servi chez AXA, où les tests faisaient partie du quotidien.


🔹 7. Utiliser des conventions nommage strictes

La structure doit être lisible en 2 secondes :

UserController
UserService
UserRepository
UserCreatedEvent
GetUserQuery
UpdateUserCommand

Pourquoi ?
➡️ Parce que le cerveau humain comprend mieux les conventions que les surprises.
➡️ Parce que l’onboard des nouveaux devs devient instantané.


🔹 8. Documenter les choix (sans écrire un roman)

Je documente uniquement ce qui compte :

  • diagramme simple (mermaid, draw.io)
  • règles importantes
  • frontières du domaine
  • modules principaux
  • flux essentiels

Tout tient souvent dans un README clair à la racine du repo.


🎯 Conclusion : une architecture simple, propre, et adaptée au métier

Il n’existe pas d’architecture parfaite.
Mais il existe une approche réaliste :

✔ garder les responsabilités claires
✔ éviter la complexité inutile
✔ découpler intelligemment
✔ préparer les tests
✔ structurer pour les évolutions futures

C’est cette approche que j’essaie d’appliquer dans tous mes projets — et c’est ce qui fait la différence entre une solution qui survit… et une solution qui prospère.

Leave a Comment