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.