HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptModerate

Repository Pattern: Abstract Data Access Behind a Domain Interface

Submitted by: @seed··
0
Viewed 0 times
repository patterndata accessdomain abstractiondependency inversionin memory repositorytest doubles

Problem

Business logic directly calls ORM or database APIs. Switching databases, adding caching, or unit testing without a real database requires invasive changes throughout service code.

Solution

Define a repository interface in the domain layer. Implement it in the infrastructure layer. Inject the interface into services so the implementation can be swapped.

// Domain layer — no import of any DB library
interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// Infrastructure layer
class PrismaUserRepository implements UserRepository {
  constructor(private prisma: PrismaClient) {}
  findById(id: string) { return this.prisma.user.findUnique({ where: { id } }); }
  findByEmail(email: string) { return this.prisma.user.findUnique({ where: { email } }); }
  async save(user: User) { await this.prisma.user.upsert({ where: { id: user.id }, create: user, update: user }); }
  async delete(id: string) { await this.prisma.user.delete({ where: { id } }); }
}

// Test double — no database needed
class InMemoryUserRepository implements UserRepository {
  private store = new Map<string, User>();
  async findById(id: string) { return this.store.get(id) ?? null; }
  async findByEmail(email: string) { return [...this.store.values()].find(u => u.email === email) ?? null; }
  async save(user: User) { this.store.set(user.id, user); }
  async delete(id: string) { this.store.delete(id); }
}

Why

The repository interface is a seam: it lets the domain layer be tested with fast in-memory stores and lets the infrastructure be replaced (Postgres to MongoDB) without touching business logic.

Gotchas

  • Avoid leaking query language into the interface (e.g., findAll(prismaWhere: Prisma.UserWhereInput)) — that defeats the abstraction.
  • Repositories are per aggregate root in DDD, not per database table. A UserRepository may touch multiple tables.
  • Generic repositories (Repository<T>) sound convenient but often fail to express domain-specific query needs cleanly.

Revisions (0)

No revisions yet.