
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
The Repository Pattern is frequently misunderstood, leading teams to build elaborate abstractions that merely wrap database calls without delivering testability or domain isolation. When implemented poorly, these layers become maintenance burdens rather than architectural assets, coupling your business logic tightly to ORM specifics. Understanding the specific Repository Pattern anti-patterns to avoid is essential before you write another interface, especially if you are also evaluating microservices vs monolith architectures where data boundaries matter significantly.
Why does exposing IQueryable rank among the worst Repository Pattern anti-patterns to avoid?
Returning IQueryable<T> from a repository method is perhaps the most pervasive mistake in modern .NET development. While it appears flexible, allowing callers to compose filters dynamically, it fundamentally violates the abstraction boundary the pattern exists to create. Your domain layer becomes implicitly coupled to Entity Framework Core’s translation capabilities, LINQ provider limitations, and database schema details.
The deferred execution trap
When a service receives an IQueryable, no SQL has executed yet. The query materializes only when the caller iterates, calls ToList(), or accesses navigation properties. This creates several operational hazards:
- N+1 queries in disguise: A developer adds
.Where(x => x.IsActive)in the service layer, unaware that this triggers lazy loading for related entities because the original repository call didn’t include.Include(). - Unpredictable performance: Database load shifts from the repository (where it can be monitored and optimized) to arbitrary service methods where execution plans are invisible to DBAs.
- Testing impossibility: You cannot unit test a service method without either hitting a real database or mocking
IQueryableproviders, which is notoriously fragile and incomplete.
// ❌ ANTI-PATTERN: Leaky abstraction
public interface IOrderRepository
{
IQueryable<Order> GetAll(); // Caller controls SQL generation
}
// ✅ CORRECT: Encapsulated query intent
public interface IOrderRepository
{
Task<IReadOnlyList<Order>> GetActiveOrdersWithItemsAsync(
DateTime sinceDate,
CancellationToken ct);
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
} The correct approach requires more upfront thought about what queries your domain actually needs. This feels restrictive initially but pays dividends when your underlying storage changes or when you need to optimize a specific slow query without refactoring ten different service classes. For teams managing complex data workflows, understanding PostgreSQL administration essentials helps clarify why pushing SQL composition upstream makes performance tuning nearly impossible.
How does the Generic Repository anti-pattern undermine domain-driven design?
The Generic Repository (IRepository<T>) promises DRY code by providing CRUD operations for every entity automatically. In practice, it treats all aggregates as identical data buckets, ignoring the rich behavioral differences that justify having separate repositories. Real domains rarely map cleanly to uniform Create/Read/Update/Delete semantics across all entities.
Where generic repositories fail
- Aggregate root violations: Not every entity should be directly accessible. Child entities within an aggregate should only be modified through their parent, but
IRepository<OrderLineItem>invites direct manipulation that bypasses invariant protection. - Query explosion: As soon as you need
GetOrdersPendingApproval(), you add it to the generic interface. Now every entity type exposes methods irrelevant to its domain, violating interface segregation. - Persistence ignorance loss: Different aggregates have different optimal storage strategies. Some benefit from document stores, others from graph databases, and some from relational tables. A single generic contract forces everything into the lowest-common-denominator relational mold.
When generic repositories are acceptable
Generic repositories aren’t inherently evil—they’re just misplaced. They work well as internal implementation details behind specific repositories, reducing boilerplate for standard persistence operations. They also suit simple administrative tools or CRUD-heavy back-office applications where domain complexity is genuinely minimal. The anti-pattern emerges when teams use them as the primary public API for rich domains.
What infrastructure leaks commonly appear in Repository Pattern implementations?
A repository should translate between domain concepts and persistence mechanisms, not expose those mechanisms upward. Infrastructure leakage occurs when ORM types, database exceptions, or storage-specific identifiers propagate into domain services, controllers, or business rules. This coupling makes migration between ORMs, databases, or even architectural styles prohibitively expensive.
Common leakage vectors
| Leak Type | Symptom | Impact | Correction |
|---|---|---|---|
| ORM Entities as Domain Models | [Table], [Column] attributes on domain classes | Domain evolves only when DB schema allows | Separate persistence models + mapping layer |
| Database Exceptions in Services | Catching SqlException in business logic | Service knows it uses SQL Server specifically | Translate to domain exceptions at repository boundary |
| Lazy Loading Surprises | Virtual navigation properties accessed post-dispose | Runtime errors depend on DbContext lifetime | Eager load explicitly or use projection DTOs |
| Change Tracker Dependencies | Entities tracked across multiple operations implicitly | Behavior changes based on scope configuration | Use detached entities + explicit attach/update |
This separation matters enormously for compliance-regulated systems. When preparing for audits under frameworks like SOC 2 or ISO 27001, demonstrating clear boundaries between business logic and infrastructure simplifies evidence collection significantly. Teams building Kubernetes secrets management pipelines encounter similar boundary issues where infrastructure concerns bleed into application code.
How do you handle complex queries without falling into Repository Pattern anti-patterns?
Real applications require sophisticated querying beyond simple lookups. The solution isn’t abandoning the pattern but adopting complementary approaches that preserve encapsulation while enabling flexibility. The Specification Pattern, CQRS read models, and explicit query objects each address different aspects of this challenge.
Specification Pattern for composable criteria
Specifications encapsulate query logic as reusable, testable domain objects rather than scattered lambda expressions. They express business intent ("overdue high-value orders") rather than technical implementation ("WHERE due_date < NOW() AND total > 1000"). Modern libraries like Ardalis.Specification integrate cleanly with EF Core while keeping domain layers persistence-agnostic.
// Domain layer - pure business intent
public class OverdueHighValueOrdersSpec : Specification<Order>
{
public OverdueHighValueOrdersSpec(DateTime asOf, decimal threshold)
{
Query.Where(o => o.DueDate < asOf && o.TotalAmount > threshold)
.Include(o => o.Customer)
.OrderByDescending(o => o.DueDate);
}
}
// Application layer - clean consumption
var overdueOrders = await _orderRepository.ListAsync(
new OverdueHighValueOrdersSpec(DateTime.UtcNow, 5000m),
cancellationToken); CQRS for read-heavy scenarios
Not every query deserves a repository method. Complex reporting, dashboard aggregations, and search interfaces often benefit from dedicated read models that bypass domain entities entirely. These projections optimize for retrieval speed and shape rather than domain purity. Separating reads from writes acknowledges that optimal storage structures differ fundamentally between transactional processing and analytical queries.
When should you skip the Repository Pattern entirely?
Not every project benefits from repository abstraction. Simple CRUD applications, prototypes validating market fit, and internal tools with short lifespans often gain nothing from the indirection. The pattern earns its keep when domain complexity warrants isolation, multiple persistence backends exist or are anticipated, or comprehensive unit testing without database dependencies is required.
Teams working with mature ORMs like EF Core already get a Unit-of-Work implementation via DbContext. Wrapping it in another repository layer sometimes adds ceremony without meaningful value, especially when the team lacks discipline about maintaining proper boundaries. Honest assessment of whether your domain complexity justifies the pattern prevents building architectural astronautics that slow delivery without improving quality.
For projects where the pattern does apply, investing time in getting it right prevents years of accumulated technical debt. Review existing implementations against these anti-patterns, refactor incrementally starting with the highest-pain areas, and establish code review guidelines that catch leaks before they merge. If your current architecture struggles with these boundaries or you need hands-on guidance implementing clean domain abstractions, reach out to discuss your specific situation.