Over the years, I have settled on two simple rules for abstractions.
Rule 1: Always abstract business logic from external dependencies
Business logic should know nothing about:
- Storage (databases, caches, file system)
- Network (HTTP clients, external APIs)
- Specific drivers and libraries
This applies to both code dependencies and naming. For example, prefer interface name CacheRepository over RedisRepository in your business logic layer - naming that references specific technologies leaks implementation details.
Why bother? You probably will not swap your database, but you might change the driver. More importantly, abstractions enable unit testing of business logic.
type CacheRepository interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
}
Do not go overboard. Loggers, tracers, metrics, and shared sync pools (in Go) are fine without extra abstractions - this depends on your language and framework conventions.
Rule 2: Be concrete outside business logic
Infrastructure code welcomes specifics. Minimal abstractions, concrete implementations, technology-specific naming.
The implementation of CacheRepository interface from business logic should be named RedisCacheRepository - be explicit about what it actually uses.
type RedisCacheRepository struct {
client *redis.Client
}
Conclusion
Abstract the core, be concrete at the edges. This separation makes refactoring safer and tests faster. The overhead is minimal, and it usually pays off.