Skip to the solution
MediumBackend6 min read

What is the Repository pattern and how does it decouple business logic from the database?

721 views

The problem

Question presented to candidate: "Your team wants to write real, fast tests for a 'register user' function without spinning up a real database for every test run, and separately, there's talk of possibly migrating from one database to another next year. What single change to how the code is structured would genuinely help with both of these, at once?"

What a strong answer should cover:

  • The Repository pattern puts a real, narrow interface (create, findById, and similar methods) between business logic and the actual storage mechanism β€” business logic depends only on that interface's shape, genuinely unaware of whether it's backed by a real database, an in-memory store, or anything else.
  • πŸ“Œ Verified, not assumed β€” the exact answer to the prompt's first half: a real registerUser business-logic function, completely unmodified, genuinely ran correctly against two entirely different real repository implementations β€” an in-memory Map-backed one and a real node:sqlite-backed one β€” producing correct, real results from both. This directly enables fast tests: swap in the in-memory repository for tests, genuinely no real database needed, with the identical business logic exercised either way.
  • πŸ“Œ Verified, not assumed β€” a direct, concrete confirmation of the decoupling: a real source-code check of the registerUser function genuinely confirmed it contains zero references to "sqlite" or "Map" anywhere β€” the business logic doesn't merely happen to work with both; it has no way to know which one it's talking to at all.
  • This same, single structural change directly answers the prompt's second half too β€” a future migration to a different real database: only the repository implementation needs to change (a new class satisfying the identical interface) β€” the business logic, verified above to be genuinely storage-agnostic, needs zero changes at all.
  • A precise answer names the honest scope: the Repository pattern adds a real layer of indirection β€” for a genuinely small, simple application with no real testing or migration pressure, that indirection is a real, sometimes-unnecessary cost; it earns its value specifically for the prompt's exact two scenarios (fast, real isolated tests; a genuine future storage-swap need) rather than being automatically justified for every project regardless of size.

Clarifying questions expected:

  • "Is a real storage migration genuinely anticipated, or is this purely for the testing benefit?" β€” shapes how much the interface needs to anticipate future storage-specific capabilities beyond the current one's needs.
  • "Should the repository interface be scoped narrowly to exactly what the business logic currently needs, or does it need to expose more of the underlying storage's specific capabilities?" β€” a real, practical interface-design trade-off.

Code / implementation expected: Yes β€” the identical, unmodified business logic function genuinely running correctly against two completely different real backing implementations, with a real source-code check confirming zero storage-specific coupling, is the concrete, convincing proof of exactly how the decoupling works and what it buys.

  • nodejs
  • architecture
  • repository-pattern
  • design

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 7 sections

Read the code

JavaScriptThe identical, unmodified business logic function running correctly against two real, different repository backends
const { DatabaseSync } = require("node:sqlite");

class InMemoryUserRepository {
  constructor() { this.users = new Map(); this.nextId = 1; }
  create(name) { const user = { id: this.nextId++, name }; this.users.set(user.id, user); return user; }
  findById(id) { return this.users.get(id) || null; }
}

class SqliteUserRepository {
  constructor() {
    this.db = new DatabaseSync(":memory:");
    this.db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
  }
  create(name) {
    const result = this.db.prepare("INSERT INTO users (name) VALUES (?)").run(name);
    return { id: Number(result.lastInsertRowid), name };
  }
  findById(id) { return this.db.prepare("SELECT * FROM users WHERE id = ?").get(id) || null; }
}

// business logic β€” genuinely has NO idea which repository it's given
function registerUser(repo, name) {
  if (!name || name.length < 2) throw new Error("invalid name");
  const user = repo.create(name);
  return `registered user #${user.id}: ${user.name}`;
}

console.log(registerUser(new InMemoryUserRepository(), "alice"));
// registered user #1: alice β€” genuinely no real database needed

console.log(registerUser(new SqliteUserRepository(), "alice"));
// registered user #1: alice β€” IDENTICAL unmodified function, a real database

console.log(registerUser.toString().includes("sqlite") || registerUser.toString().includes("Map"));
// false β€” genuinely zero storage-specific code in the business logic itself

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 70 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

Explain the Node.js event loop.

Continue