Introduction
The Repository Pattern abstracts persistence. CQRS separates read and write responsibilities.
Understanding this distinction is important because misuse of either pattern often leads to unnecessary complexity. Many systems end up with over engineered repositories or premature CQRS implementations.
Repository Pattern in practice
The Repository Pattern abstracts data access behind an interface that behaves like a collection of aggregates.
The goal is not to hide the database completely. The goal is to keep persistence logic outside the domain and application services.
Typical interface:
public interface IOrderRepository
{
Order GetById(Guid id);
IEnumerable<Order> GetByCustomerId(Guid customerId);
void Add(Order order);
void Update(Order order);
void Remove(Order order);
}
In a typical layered architecture the flow looks like this:
UI
↓
Application Service
↓
Repository
↓
ORM / Data Access
↓
Database
Application services work with aggregates while the repository handles persistence.
Example usage inside an application service:
public class OrderService
{
private readonly IOrderRepository repository;
public OrderService(IOrderRepository repository)
{
this.repository = repository;
}
public void CreateOrder(Guid customerId)
{
var order = new Order(customerId);
repository.Add(order);
}
}
This keeps the service focused on business behavior rather than persistence mechanics.
Repository pattern limitations
Repositories work well when the domain model aligns with the data model. Problems begin when read scenarios diverge from aggregate boundaries.
Typical issues include:
Query inflation
Repositories accumulate query specific methods.
GetOrdersByDateRange()
GetOrdersByStatus()
GetOrdersForDashboard()
GetOrdersWithCustomerAndPayments()
Eventually repositories become read APIs.
Inefficient read models
Returning domain entities for read operations forces unnecessary object graphs.
Example:
Order -> OrderLines -> Product -> Supplier
Even when the UI only needs:
OrderId
CustomerName
Total
Status
Generic repository anti pattern
Many projects introduce a generic repository abstraction.
IGenericRepository<T>
This usually becomes a thin wrapper around the ORM and adds little value.
CQRS fundamentals
CQRS starts from a simple observation.
Reading data and modifying data are fundamentally different operations.
Commands change state. Queries return data.
CQRS formalizes this separation by introducing different models for each concern.
Command side
Commands represent business intent.
Example command:
public class CreateOrderCommand
{
public Guid CustomerId { get; set; }
}
Handled by a command handler:
public class CreateOrderHandler
{
private readonly IOrderRepository repository;
public CreateOrderHandler(IOrderRepository repository)
{
this.repository = repository;
}
public void Handle(CreateOrderCommand command)
{
var order = new Order(command.CustomerId);
repository.Add(order);
}
}
Commands usually operate on domain aggregates.
Query side
Queries retrieve data optimized for the consumer.
Example query:
public class GetOrderSummaryQuery
{
public Guid OrderId { get; set; }
}
Query handler:
public class GetOrderSummaryHandler
{
private readonly IDbConnection connection;
public GetOrderSummaryHandler(IDbConnection connection)
{
this.connection = connection;
}
public OrderSummaryDto Handle(GetOrderSummaryQuery query)
{
const string sql = @"
SELECT
o.Id,
c.Name AS CustomerName,
o.Total,
o.Status
FROM Orders o
JOIN Customers c ON c.Id = o.CustomerId
WHERE o.Id = @Id";
return connection.QuerySingle<OrderSummaryDto>(sql, new { Id = query.OrderId });
}
}
The query side returns DTOs designed specifically for the UI or API.
Architectural comparison
The important difference between the patterns is scope.
Aspect | Repository Pattern | CQRS |
|---|---|---|
Primary concern | Persistence abstraction | Responsibility separation |
Architecture level | Data access | Application architecture |
Model structure | Single domain model | Separate read and write models |
Complexity | Low | Medium to high |
Repositories are an implementation detail. CQRS is an architectural style.
How CQRS and repositories work together
In real systems the most common arrangement is:
Command side uses repositories.
Query side bypasses them.
Command flow:
UI
↓
Command
↓
Command Handler
↓
Domain Aggregate
↓
Repository
↓
Database
Query flow:
UI
↓
Query
↓
Query Handler
↓
SQL / Dapper
↓
Database
Repositories remain responsible for aggregate persistence while query handlers use optimized SQL or projections.
This avoids turning repositories into read APIs.
When repositories are enough
Many applications do not benefit from CQRS.
Repositories alone are usually sufficient when:
The application is CRUD focused
Domain logic is moderate
Read requirements align with aggregates
System scale is predictable
Typical architecture:
Controllers
↓
Application Services
↓
Repositories
↓
ORM
↓
Database
This architecture is simple and easy to maintain.
When CQRS becomes useful
CQRS becomes valuable when read complexity diverges from write complexity.
Common scenarios include:
High read throughput
Systems with large numbers of read requests.
Complex dashboards or reporting
Queries become difficult to express through aggregates.
Different read models
UI models differ significantly from domain models.
Example:
Write model:
Order
OrderLine
Payment
Shipment
Read model:
OrderSummary
CustomerOrders
DailySalesDashboard
These projections often require denormalized structures.
A pragmatic adoption strategy
CQRS should rarely be introduced at the beginning of a project.
A typical evolution path looks like this:
Layered architecture
↓
Repositories and domain services
↓
Introduce query handlers for complex reads
↓
Adopt full CQRS separation
This keeps complexity proportional to system needs.
Final thoughts
The Repository Pattern and CQRS are frequently discussed together which makes them appear interchangeable. In reality they address different concerns.
Repositories manage how aggregates are persisted.
CQRS manages how the application handles reads and writes.
In most modern .NET systems the practical architecture looks like this:
Command Side
Command → Handler → Domain → Repository → Database
Query Side
Query → Handler → SQL/Dapper → Database
This approach preserves the benefits of domain modeling on the write side while allowing highly optimized reads.
For many systems repositories alone are sufficient. CQRS becomes valuable when the complexity of read models starts to diverge from the domain model.
Recognizing that moment is an architectural judgment call that typically comes with experience.