Skip to solution
mediumBackend

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

721 views
01

Understand 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.

nodejsarchitecturerepository-patterndesign
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js architecture and testing interviews — assumes familiarity with the unit-vs-integration-testing question's real speed-comparison proof. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interv

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

The 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
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 70 of 152 decoded in the Node.js track. One more won't hurt.

Back to track