Repository Pattern Anti-Patterns to Avoid

Khimananda Oli 8 min read Web Development
Repository Pattern Anti-Patterns to Avoid

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.

Correct Abstraction vs Leaky RepositoryDomain ServiceBusiness Rules OnlyProper RepositoryReturns Domain ObjectsDatabase / ORMEF Core / DapperDomain ServiceForced to FilterLeaky RepositoryExposes IQueryableDatabase / ORMSQL Logic Leaks UpTop: Encapsulated Persistence | Bottom: Distributed SQL Logic
Visual comparison of encapsulated repository abstraction versus leaky IQueryable anti-pattern flow

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 IQueryable providers, 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

  1. 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.
  2. 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.
  3. 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.
Generic Repository MisuseIRepository<T> : CRUD for AllAdd / Update / Delete / GetById / GetAllOrderDirect LineItemAccess AllowedCustomerSame InterfaceNo SpecializationProductInventory LogicIgnored by Base⚠ Broken Invariants⚠ Bloated Interfaces⚠ Storage HomogenizationDedicated RepositoriesAggregate-Specific ContractsTailored Methods Per RootIOrderRepositorySubmitOrder()GetPendingApprovals()Protects LineItemsICustomerRepositoryFindByEmail()GetPurchaseHistory()Enforces Privacy RulesIInventoryRepositoryReserveStock()CheckAvailability()Optimistic Concurrency✓ Protected Aggregates✓ Intent-Revealing APIs✓ Polyglot Persistence Ready
Side-by-side comparison of generic repository misuse versus dedicated aggregate-specific repository contracts

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 TypeSymptomImpactCorrection
ORM Entities as Domain Models[Table], [Column] attributes on domain classesDomain evolves only when DB schema allowsSeparate persistence models + mapping layer
Database Exceptions in ServicesCatching SqlException in business logicService knows it uses SQL Server specificallyTranslate to domain exceptions at repository boundary
Lazy Loading SurprisesVirtual navigation properties accessed post-disposeRuntime errors depend on DbContext lifetimeEager load explicitly or use projection DTOs
Change Tracker DependenciesEntities tracked across multiple operations implicitlyBehavior changes based on scope configurationUse 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.

Choosing the Right Query StrategyNew Query Need?Start HereIs It Reusable Business Criteria?(e.g., "eligible for discount")YESNOSpecification PatternEncapsulate as Domain ObjectTest IndependentlyIs It Read-Only / Reporting?Dashboard, Search, ExportHigh Volume / Complex JoinsCQRS Read Model / ProjectionBypass Domain Entities EntirelyOptimize Shape + PerformanceSimple Lookup?Just Add Repository Method
Decision flowchart guiding engineers toward specifications, CQRS, or simple repository methods based on query characteristics

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.

Frequently Asked Questions

No, but misapplying it creates anti-patterns. In 2026 Laravel projects, wrapping Eloquent unnecessarily adds complexity without value. Use repositories only when abstracting multiple data sources or enforcing strict domain boundaries, not as a default architectural layer for simple CRUD applications.

It couples your domain logic directly to the database ORM implementation. Changes to schema break business logic unexpectedly. Return DTOs or domain entities instead to maintain true abstraction and testability across your application layers.

Only if implemented correctly with interfaces. Many developers create repositories that still depend on global state or static facades, negating test benefits. True isolation requires dependency injection and pure return types, not just a wrapper class around Eloquent queries.

No. This one-to-one mapping is a common anti-pattern creating boilerplate without architectural benefit. Create repositories only for complex aggregate roots or when query logic exceeds simple model scopes. Simple reads should use Eloquent directly.

They often violate the Interface Segregation Principle by forcing unused methods onto specific implementations. A generic IRepository with twenty methods bloats concrete classes. Define narrow, purpose-specific interfaces tailored to actual consumer needs rather than attempting universal data access abstractions.

Fat repositories accumulate business logic, validation, and transformation code that belongs in services or domain models. Repositories should handle only data persistence and retrieval. Move orchestration logic to action classes or domain services to maintain single responsibility.

Yes. Hiding eager loading behind repository methods obscures query performance. Callers cannot optimize loads without modifying the repository interface. Expose specification objects or allow passing eager-load constraints to prevent hidden performance regressions in production environments.

Read models typically bypass repositories entirely, querying projections directly. Write-side repositories may still encapsulate aggregate persistence, but command handlers often work with domain services instead. Forcing repositories into both sides of CQRS adds unnecessary indirection and complexity.

New team members trace through multiple interface layers to understand simple data flows. Excessive abstraction hides intent and increases cognitive load. Prefer explicit, readable Eloquent queries for straightforward operations and reserve repository patterns for genuinely complex domain boundaries requiring isolation.

Leaky abstractions expose raw query builders or allow arbitrary filter injection. Unvalidated parameters passed through repository methods can enable SQL injection or unauthorized data access. Always validate and sanitize inputs at the service layer before reaching persistence code.

Rarely in practice. Most repository implementations still contain Eloquent-specific syntax, relationships, or query builder calls. True portability requires complete ORM abstraction, which sacrifices framework productivity. Accept vendor lock-in unless multi-database support is a confirmed business requirement.

When read logic grows complex but write operations remain simple. Query objects encapsulate specific reporting or search requirements without polluting repository interfaces. This separation keeps persistence concerns focused while allowing optimized, denormalized reads independent of domain aggregates.

Repositories managing their own transactions create implicit coupling between unrelated operations. Transaction management belongs at the service or use-case level where business workflows are orchestrated. Let repositories participate in externally managed transactions rather than controlling commit scope internally.

Yes. Mixing caching with persistence violates single responsibility and makes cache invalidation difficult to reason about. Apply caching at the service layer or via middleware where business context determines appropriate TTLs and invalidation strategies based on domain events.

Use Eloquent scopes, query builders, or dedicated read models for complex retrieval. Action classes handle write workflows. Reserve repositories strictly for aggregate root persistence in DDD contexts. Match abstraction level to actual complexity rather than applying patterns dogmatically.