Service Repository Pattern
- • Adds architectural complexity for simple CRUD applications
- • Requires discipline to maintain separation
- • Initial development is slower than direct database access
Problem
Monolithic applications often mix business logic with database operations:
// ❌ Tightly coupled code
class UserController {
async getUser(id: string) {
const user = await db.query('SELECT * FROM users WHERE id = ?', [id])
if (!user) throw new Error('Not found')
if (user.is_suspended) throw new Error('Account suspended')
return user
}
}
Issues:
- Business logic (suspension check) mixed with data access (SQL query)
- Hard to test without a real database
- Can't reuse logic across different controllers
- Difficult to swap databases or add caching
Solution
Separate concerns into distinct layers:
Repository Layer (Data Access):
interface UserRepository {
findById(id: string): Promise<User | null>
save(user: User): Promise<void>
}
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
const row = await this.db.query('SELECT * FROM users WHERE id = ?', [id])
return row ? this.toDomain(row) : null
}
}
Service Layer (Business Logic):
class UserService {
constructor(private repo: UserRepository) {}
async getUser(id: string): Promise<User> {
const user = await this.repo.findById(id)
if (!user) throw new UserNotFoundError(id)
if (user.isSuspended) throw new AccountSuspendedError(user.id)
return user
}
}
Controller (Coordination):
class UserController {
constructor(private service: UserService) {}
async handleGetUser(req: Request): Promise<Response> {
const user = await this.service.getUser(req.params.id)
return { status: 200, body: user }
}
}
Adding Caching Layer
Enhance performance by adding cache between Service and Repository:
class CachedUserRepository implements UserRepository {
constructor(
private cache: Cache,
private repo: UserRepository
) {}
async findById(id: string): Promise<User | null> {
// Check cache first
const cached = await this.cache.get(`user:${id}`)
if (cached) return cached
// Cache miss: query database
const user = await this.repo.findById(id)
if (user) {
await this.cache.set(`user:${id}`, user, { ttl: 300 })
}
return user
}
}
Benefits
- Testability: Mock repositories for unit testing
- Maintainability: Changes to data access don't affect business logic
- Flexibility: Swap databases without changing services
- Reusability: Services can be used by multiple controllers
- Performance: Easy to add caching without changing business logic
Real-World Examples
Amazon: Product catalog service uses repository pattern with Redis caching, reducing database load by 90%.
Netflix: Content metadata service abstracts Cassandra, allowing migration to other stores without service changes.
Uber: Ride service separates business rules from geo-spatial database queries, enabling independent optimization.
When to Use
Use this pattern when:
- Business logic is complex
- Multiple controllers need same data operations
- System requires comprehensive testing
- Data sources may change
- Performance optimization (caching) is needed
Skip this pattern when:
- Application is simple CRUD
- Using an ORM that already provides abstraction
- Team is small and architecture discipline is low
- Deadlines are tight and complexity isn't justified
Implementation Checklist
- Define repository interfaces (CRUD operations)
- Implement repository for chosen database
- Create service layer with business logic
- Use dependency injection for testability
- Write unit tests with mocked repositories
- Add caching layer if needed
- Document repository contracts
- Handle errors at appropriate layers
Common Pitfalls
Over-abstraction: Don't create repositories for every table—group related data operations.
Leaky abstractions: Don't expose database-specific types (e.g., SQL result objects) from repositories.
Anemic services: Services that just pass through to repositories add no value.
Generic repositories: One-size-fits-all repositories are hard to use and maintain.
Trade-offs
Pros:
- Clean separation of concerns
- Easy to test
- Flexible and maintainable
Cons:
- More code to write initially
- Requires architectural discipline
- Overkill for simple applications
This pattern is not about perfection—it's about managing complexity as systems grow.
Related Content
⚛conceptConcepts
- Service LayerA layer in software architecture that encapsulates business logic and coordinates application operations.
- Repository PatternA design pattern that encapsulates data access logic, providing a collection-like interface for domain objects.
- CachingA technique for storing frequently accessed data in fast-access memory to reduce latency and database load.