Zruck zum Blog, du Zniachtl
Mai 2026·3 Min. zum LesnVo da KI gmocht

Des Repository-Muster: Vor- und Nachteile, und wann ma's weglassn soit

#architecture#design-patterns#backend

Des Repository-Muster wird in vüle Backend-Tutorials standardmäßig empfohlen, gleich neben Dependency Injection, ois wär's offensichtlich falsch, es wegzulassn. Is es ned. Es is a wirklich nützliches Muster für a bestimmte Menge an Problemen, und wirklich unnötiger Overhead für vüle Anwendungen, de's trotzdem aus Gewohnheit übernehmen.

Wos es tatsächlich is

A Repository is a Abstraktion zwischn deiner Domänen-/Geschäftslogik und deim Datenzugriffs-Code. Statt dass deine Service-Schicht an ORM aufruaft oda direkt SQL schreibt, ruaft sie a Repository-Interface auf, und das Repository is das Einzige, des woaß, wia die Daten tatsächlich gspeichert wern.

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

A konkrete Implementierung kunnt an ORM nutzn:

class PostgresUserRepository implements UserRepository {
  constructor(private db: Pool) {}

  async findById(id: string): Promise<User | null> {
    const { rows } = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
    return rows[0] ? mapRowToUser(rows[0]) : null;
  }

  async findByEmail(email: string): Promise<User | null> {
    const { rows } = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
    return rows[0] ? mapRowToUser(rows[0]) : null;
  }

  async save(user: User): Promise<void> {
    await this.db.query(
      'INSERT INTO users (id, email, name) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET email = $2, name = $3',
      [user.id, user.email, user.name],
    );
  }
}

Dein Service-Code hängt nur von UserRepository ab, nie direkt von Pool oda SQL:

class RegisterUserService {
  constructor(private users: UserRepository) {}

  async register(email: string, name: string): Promise<User> {
    const existing = await this.users.findByEmail(email);
    if (existing) throw new Error('email already registered');

    const user = new User(crypto.randomUUID(), email, name);
    await this.users.save(user);
    return user;
  }
}

Das Plädoyer dafür

Testbarkeit ohne echte Datenbank. A Fake-Implementierung von UserRepository is trivial zum Schreibn und braucht koane Test-Container, koane Migrationen, ka Netzwerk:

class InMemoryUserRepository implements UserRepository {
  private users = new Map<string, User>();

  async findById(id: string) {
    return this.users.get(id) ?? null;
  }

  async findByEmail(email: string) {
    return [...this.users.values()].find((u) => u.email === email) ?? null;
  }

  async save(user: User) {
    this.users.set(user.id, user);
  }
}

Tests gegn RegisterUserService laufn in Millisekunden und brauchn koa Infrastruktur.

A echte Nahtstelle, wenn si der Speicher wirklich ändert. Wennst von Postgres auf DynamoDB migrierst oda a Caching-Schicht vor die Lesezugriffe schaltest, is das Repository-Interface der oane Ort, der si ändert - jeder Aufrufer bleibt unberührt, weil er nie vom Speichermechanismus abhängig woar.

Oa zentraler Platz, um Query-Konventionen durchzusetzen. Soft-Delete-Filterung, Mandanten-Scoping in am Multi-Tenant-System, konsistentes Spalten-Mapping - dass im Repository zu zentralisiern heißt, jeder Aufrufer kriagt's automatisch, statt dass sich jede Aufrufstelle drum merken muass.

Das Plädoyer dagegen

Es is a Abstraktion für a Änderung, de meist ned passiert. In da Praxis wechseln sehr wenige Anwendungen tatsächlich ihre Datenbank-Engine. Das größte Verkaufsargument vom Repository-Muster - "du kannst die Persistenz ändern, ohne die Geschäftslogik anzufassn" - is a Versicherung gegn a Ereignis, des bei de meisten Projekten nie eintritt. Du zoist die Kosten (a zusätzliches Interface, a zusätzliche Implementierung, a zusätzliche Schicht zum Durchwandern beim Codelesen) jeden Tag für an Nutzen, den du vielleicht nie einkassierst.

Moderne ORMs isolieren di scho von rohem SQL. A Teil der ursprünglichen Motivation fürs Repository-Muster woar, SQL aus da Geschäftslogik rauszuhoitn. Der Query Builder von an ORM mocht des scho. An ORM-Aufruf in a Repository-Methode zu wickeln, de nix ois den Aufruf weiterreicht, fügt a Schicht dazu, ohne Isolation, die du ned eh scho hättest:

// This "repository" method isn't abstracting anything the ORM wasn't already abstracting.
async findById(id: string) {
  return this.db.user.findUnique({ where: { id } });
}

Es kann die tatsächlich laufende Query hinter am Nama verstecken, der dir seine Kosten ned verrät. orderRepository.findActiveWithLineItems() schaut an der Aufrufstelle harmlos aus. Ob es oane indizierte Query oda a N+1-Schleife über Bestellpositionen is, bleibt unsichtbar, bis du die Implementierung liest - die Abstraktion, die Code leichter durchschaubar machn soit, endet dabei, Performance-Eigenschaften schwerer sichtbar zu machn.

De meisten Test-Suites brauchn gor ned wirklich a ausgetauschte Datenbank. Test-Container, de a echtes Postgres in Docker laufn lassn, sand schnell und zuverlässig gnua gworn, dass "ohne echte Datenbank testn" a schwächeres Argument is ois früher - und gegn das echte Ding zu testn fangt Probleme ab (a schlechter Index, a Constraint-Verletzung, a echter Query-Plan), de a In-Memory-Fake ned fangen kann.

A Mittelweg

Für a klane bis mittlere Anwendung, de mit oaner Datenbank redt, die sie realistischerweise ned austauscht, bringt a dünne Datenzugriffsschicht, direkt auf dem Query Builder von deim ORM aufgebaut - ohne Repository-Interface dahinter -, den Großteil vom organisatorischen Nutzen (Queries zentralisiert, ned über die Geschäftslogik verstreut) ohne die zusätzliche Indirektion:

// db/users.ts — plain functions, no interface, no DI required
export async function findUserByEmail(db: PrismaClient, email: string) {
  return db.user.findUnique({ where: { email } });
}

export async function saveUser(db: PrismaClient, user: User) {
  return db.user.upsert({
    where: { id: user.id },
    update: user,
    create: user,
  });
}

Tests kinnan trotzdem a Testdatenbank einsetzen oda das Transaktions-Rollback-pro-Test-Muster von deim ORM nutzn, ohne an Interface, das eigens zum Austauschen gebaut wurde.

Wann ma wirklich dazu greift

  • Du baust a Bibliothek oda a Modul, des wirklich mehrere Backends unterstützen muass (a selbst gehostete App, de sowohl gegn Postgres ois a gegn SQLite laufn muass, zum Beispiel).
  • Die Domänenlogik is komplex gnua, dass du Unit-Tests ohne jeden I/O willst, und a echte Testdatenbank in deiner CI-Umgebung zu langsam oda ned verfügbar is.
  • Du machst bewusst Domain-Driven Design mit am reichen Domänenmodell, des gor ned wissn soit, dass Persistenz existiert.

Außerhalb davon is a Repository-Interface mit genau oaner Implementierung, die du je schreibst, auf Kredit bezahlte Komplexität gegn a Zukunft, de bei de meisten Projekten ned kummt.

NeicherA einfache Job-Queue mit Redis bauenElterDatenbank-Transaktionen und Isolationslevel erklärt