Aller au contenu principal
Design Patterns

Le Pattern Repository

6 min de lecture
  • design-patterns
  • architecture
  • programmation
Repository : séparer le métier de la persistance

Vous ouvrez un service métier et vous tombez sur du SQL brut mélangé avec des validations, des calculs de prix et des appels à la base de données. Tout est imbriqué. Les tests sont impossibles sans une vraie base. Le moindre changement de base de données casse la moitié du code. Le pattern Repository existe pour résoudre exactement ce problème.

Le problème : le mélange des genres

Voici un cas classique — un service qui fait tout à la fois :

class ArticleService {
  async publier(articleId: string): Promise<void> {
    // Accès aux données mélangé avec la logique métier
    const rows = await db.query(
      "SELECT * FROM articles WHERE id = $1", [articleId]
    );
    const article = rows[0];
 
    if (!article) throw new Error("Article introuvable");
    if (article.status === "published") throw new Error("Déjà publié");
    if (!article.title || !article.content) {
      throw new Error("Article incomplet");
    }
 
    await db.query(
      "UPDATE articles SET status = 'published', published_at = NOW() WHERE id = $1",
      [articleId]
    );
  }
}

Trois problèmes immédiats :

  • Impossible à tester sans base de données réelle
  • Couplage fort — changer de base (PostgreSQL vers MongoDB) implique de réécrire la logique métier
  • Duplication — les mêmes requêtes SQL se retrouvent dans plusieurs services

Le Repository : un contrat entre le métier et la donnée

Le Repository est une abstraction qui expose une interface de collection pour accéder aux données. Le code métier ne sait pas comment les données sont stockées — il manipule des objets à travers un contrat.

interface ArticleRepository {
  findById(id: string): Promise<Article | null>;
  findPublished(): Promise<Article[]>;
  save(article: Article): Promise<void>;
  delete(id: string): Promise<void>;
}

Le service métier ne dépend plus que de cette interface :

class ArticleService {
  constructor(private articles: ArticleRepository) {}
 
  async publier(articleId: string): Promise<void> {
    const article = await this.articles.findById(articleId);
 
    if (!article) throw new Error("Article introuvable");
    article.publier(); // La logique métier est dans l'entité
 
    await this.articles.save(article);
  }
}

Le SQL a disparu du service. La logique métier est isolée. Le code est lisible.

Les implémentations concrètes

En production : SQL

class SqlArticleRepository implements ArticleRepository {
  constructor(private db: Database) {}
 
  async findById(id: string): Promise<Article | null> {
    const row = await this.db.query(
      "SELECT * FROM articles WHERE id = $1", [id]
    );
    return row ? this.toArticle(row) : null;
  }
 
  async save(article: Article): Promise<void> {
    await this.db.query(
      `INSERT INTO articles (id, title, content, status, published_at)
       VALUES ($1, $2, $3, $4, $5)
       ON CONFLICT (id) DO UPDATE SET
         title = $2, content = $3, status = $4, published_at = $5`,
      [article.id, article.title, article.content,
       article.status, article.publishedAt]
    );
  }
 
  private toArticle(row: any): Article {
    return new Article(row.id, row.title, row.content,
                       row.status, row.published_at);
  }
}

En test : in-memory

class InMemoryArticleRepository implements ArticleRepository {
  private articles = new Map<string, Article>();
 
  async findById(id: string): Promise<Article | null> {
    return this.articles.get(id) ?? null;
  }
 
  async findPublished(): Promise<Article[]> {
    return [...this.articles.values()]
      .filter(a => a.status === "published");
  }
 
  async save(article: Article): Promise<void> {
    this.articles.set(article.id, article);
  }
 
  async delete(id: string): Promise<void> {
    this.articles.delete(id);
  }
}

Maintenant, tester le service devient trivial :

test("publier un article complet", async () => {
  const repo = new InMemoryArticleRepository();
  const article = new Article("1", "Mon titre", "Mon contenu", "draft", null);
  await repo.save(article);
 
  const service = new ArticleService(repo);
  await service.publier("1");
 
  const result = await repo.findById("1");
  expect(result?.status).toBe("published");
});

Pas de base de données. Pas de mock complexe. Un test rapide, fiable, isolé.

Les bénéfices concrets

Testabilité — L'implémentation in-memory permet de tester toute la logique métier sans infrastructure. Les tests passent en millisecondes.

Interchangeabilité — Migrer de PostgreSQL vers MongoDB ? Vous écrivez une nouvelle implémentation du Repository. Le reste du code ne change pas.

Séparation des responsabilités — La logique métier vit dans les entités et les services. L'accès aux données vit dans le Repository. Chaque partie évolue indépendamment.

Centralisation des requêtes — Fini les requêtes SQL dupliquées dans dix fichiers différents. Toutes les requêtes pour une entité sont regroupées dans un seul endroit.

Quand ne PAS utiliser le Repository

Le pattern a un coût : une interface, au moins deux implémentations, une couche de mapping. Ce coût est justifié quand la logique métier est riche. Il ne l'est pas toujours.

  • CRUD simple — Si votre service ne fait que lire et écrire sans logique, un accès direct à la base (via un ORM) suffit. Le Repository ajouterait de la cérémonie sans valeur.
  • Prototype ou MVP — Quand vous explorez une idée, l'abstraction prématurée vous ralentit.
  • Micro-service très fin — Un service qui expose une seule table avec des opérations basiques n'a pas besoin de cette indirection.

Le lien avec DDD et Clean Architecture

Le Repository est un concept central du Domain-Driven Design. Dans le DDD, chaque Aggregate a son Repository. Le domaine définit l'interface, l'infrastructure fournit l'implémentation. L'Aggregate ne sait jamais comment il est persisté.

En Clean Architecture et en architecture hexagonale, le Repository joue le rôle de Port (côté domaine) et d'Adapter (côté infrastructure). C'est l'application directe du principe d'inversion de dépendances : le domaine ne dépend de rien, c'est l'infrastructure qui dépend du domaine.


Le Repository n'est pas qu'un pattern d'accès aux données — c'est un contrat qui protège votre logique métier de la complexité de la persistance. Il rend votre code testable, découplé et évolutif. Mais comme tout pattern, il a un coût. Utilisez-le là où la complexité métier le justifie, pas comme un réflexe systématique.