The Repository Pattern
Business logic littered with SQL queries or detailed knowledge of how the ORM saves data has a problem: it couples the application's brain to its storage. Change the schema, and the ripple hits your domain rules.
A Repository hides the how from the what. Think of it as a collection your business logic can query without knowing where the data actually lives. Your domain code doesn't "query a table" or worry about joins. It asks for what it needs:
// Without Repository: Coupled to persistence details
async function processRecentUsers() {
const users = await db.query(
"SELECT * FROM users WHERE status = 'active' AND last_login > $1",
[thirtyDaysAgo]
);
// business logic here
}
// With Repository: Focused on domain intent
async function processRecentUsers() {
const users = await userRepository.findActiveRecent();
// business logic here
}
The Repository handles the translation. It knows about databases, caching, and schemas — those details are its responsibility, not the domain's:
class UserRepository {
async findActiveRecent(): Promise<User[]> {
// All the SQL complexity lives here
const rows = await db.query(
"SELECT * FROM users WHERE status = 'active' AND last_login > $1",
[thirtyDaysAgo]
);
return rows.map(row => new User(row));
}
}
When storage changes — swapping a local database for an API, adding a cache layer, migrating to a new schema — you update the Repository. The rest of the application never knows. Your domain logic stays focused on the problem it's actually solving, not on where the data happens to live today.