Repository that “just caches a bit” — starring Jira CTN-427 (and friends)
By Fuat Can Koseoglu,
PR smell: PlayerRepository.CreatePlayer() now needs six services and sends notifications. Your repository (repo) just graduated from data access to orchestration and side effects. • Month 1: Clean PlayerRepository (Get, Save, Delete) • Month 2–3: “Add SendWelcomeEmail() / cache leaderboards for performance.” • Month 6: Pulls IEmailService, INotificationService, IAuditLogger, ICacheService… Now your repo is the app layer in disguise—classic God-Object drift. Why this happens • Performance paranoia: “Fewer calls = faster.” • Convenience creep: “Repo already has the data.” • Feature pressure: “Just add it here.” The Real Production Cost • Transactions → Rollbacks undo DB writes, not external effects • Deadlocks/retries → Long transactions + network calls magnify locks • Tests → Unit tests boot the whole world • Coupling → Small repo edits break multiple features • N+1 perf → Convenience reads hide extra queries • Caching → Self-cached repos drift stale • Integration sprawl → External API calls sneak into repos • Side effects → In-txn notifications cause inconsistency The Pattern That Scales (Roles) • Repository: DB-only (Get/Find/Save/Update/Delete), stateless, single DB concern • Application service: Orchestrates use cases + transactions, calls repos & external APIs, emits Outbox messages • Domain service: Pure business logic (rules/invariants), no I/O • Infrastructure: Caching, email, notifications, analytics — each with single responsibility PR Litmus — Quick Quality Gates • DB-only repo: No external calls, no verbs like Send* or Notify* • Stateless: No fields beyond DB context/connection • Transaction ownership: Repo never calls SaveChanges(); Unit of Work/app service owns it • Queries: No IQueryable leakage; return entities/projections (or use query interface) • Read/write split: Complex reads via CQRS query handlers • Side effects: Routed via app service + Outbox (idempotent handlers) • Caching: Implement via decorator/layer; repos remain stateless Key insight: If your PlayerRepository needs anything beyond database access, it isn’t a repository — it’s a confused application service!