Principles
Depend on interfaces, not implementations
Services, repositories, gateways, and any other dependency should never receive or reference a concrete class directly. Always depend on an interface (contract) and inject the implementation.
typescript
// ✅ Correct — depends on interface
class UsersService implements IUsersService {
constructor(private readonly repository: IUsersRepository) {}
}
// ❌ Wrong — depends on concrete class
class UsersService {
constructor(private readonly repository: UsersRepository) {}
}This applies at every layer:
- Controllers depend on a service interface (
IUsersService), never onUsersService - Services depend on a repository interface (
IUsersRepository), never onUsersRepository - Services depend on gateway interfaces (
IPaymentGateway), never onStripeGateway
This makes your code testable (swap real implementations for mocks), decoupled (change the database without touching the service), and explicit about its contracts.
