Repository is a frequently misunderstood pattern in DDD. In many codebases, Repository is used almost like a DAO. An object that reads data from a database, updates specific columns, and executes necessary queries gets the name Repository.
Of course, Repository is related to storage. But in DDD, the purpose of Repository is not simply to hide SQL. Repository provides a collection-like abstraction that handles the lifecycle of Aggregates so that the domain model is not pulled by storage technology.
This difference may look small, but it is important. Depending on how we understand Repository, the domain model can remain centered on behavior, or it can fall back into data modification.
DAO and Repository have different concerns
DAO is a data access object. Its center is the technical concern of reading from and writing to tables.
orderDao.updateStatus(orderId, "CANCELED");
orderDao.findOrderItems(orderId);
orderDao.insertOrderHistory(orderId, status);
This code expresses database manipulation well. But domain behavior is weakly visible. We do not see “cancel the order.” We only see that the status column is updated to CANCELED.
Repository in DDD has a different perspective.
Order order = orderRepository.findById(orderId);
order.cancel(reason, now);
orderRepository.save(order);
Here, the core behavior is order.cancel(). Repository only retrieves the order and saves it again. Domain rules are inside the Aggregate, not inside the Repository.
Repository is not the center of database manipulation. It is the entrance through which we meet domain objects again.
Handle Aggregate Root units
In DDD, Repository is usually placed at the Aggregate Root level. We do not create Repositories for every object inside an Aggregate.
Suppose a Subscription Aggregate contains objects such as SubscriptionPeriod, PlanSnapshot, and RenewalHistory. If these objects are components inside the Aggregate, there is no need to create a Repository for each one.
SubscriptionRepository
This single Repository handles the lifecycle of Subscription, the Aggregate Root.
What matters is not the unit of storage, but the unit of consistency. The Aggregate Root is the entry point accessed from outside, and the Repository loads and saves that Aggregate Root.
Subscription subscription = subscriptionRepository.findById(subscriptionId);
subscription.renew(paymentResult, now);
subscriptionRepository.save(subscription);
External code does not directly save or modify objects inside the Aggregate. It changes them only through the Aggregate Root. This is how the Aggregate can protect its invariants.
It is dangerous when Repository bypasses domain behavior
When Repository starts being used like a DAO, the domain model easily becomes anemic.
subscriptionRepository.updateStatus(subscriptionId, SubscriptionStatus.CANCELED);
subscriptionRepository.updateExpiredAt(subscriptionId, now);
subscriptionRepository.insertCancelHistory(subscriptionId, reason);
This code may look fast and efficient. But it has an important problem. It bypasses domain rules such as whether the subscription is in a cancelable state, which events should occur when it is canceled, and how canceling during a free trial differs from canceling a paid subscription.
The reason domain objects exist is not simply to hold data. It is to protect their own rules. If Repository starts changing state directly, the domain object is no longer the center that protects rules.
A good flow is closer to this:
Subscription subscription = subscriptionRepository.findById(subscriptionId);
subscription.cancelByMember(reason, now);
subscriptionRepository.save(subscription);
In this structure, state changes happen through domain object behavior. Repository does not bypass behavior.
What it means to look like a collection
One important sense Eric Evans describes about Repository is that it looks like a collection. It means treating it as if we find, add, and remove needed objects from an in-memory collection of objects.
Subscription subscription = subscriptions.findById(subscriptionId);
subscriptions.save(subscription);
This expression is closer to a collection of domain objects than to a database table. Of course, the internal implementation may be SQL, JPA, or a document database. But the domain layer does not need to know that.
Repository interfaces should be defined in language needed by the domain.
public interface SubscriptionRepository {
Optional<Subscription> findById(SubscriptionId id);
void save(Subscription subscription);
}
If needed, query methods with domain meaning can exist.
Optional<Subscription> findActiveByMemberId(MemberId memberId);
But if Repository starts taking responsibility for every screen query requirement, confusion appears again. Complex searches, list screens, statistics, and reports may be better handled by a separate Query model or read-only Repository than by a domain Repository.
Distinguish Command and Query
Aggregate Repository is better focused on bringing a model for change. Aggregate is a model for protecting invariants and changing state, not for satisfying every query screen.
Suppose an admin screen needs to show a subscription list. The needed information may be:
Member email
Plan name
Subscription status
Latest payment date
Next renewal date
Customer grade
Whether payment is overdue
Loading the entire Subscription Aggregate and following related member and payment information to compose this data may not be a good choice. In this case, using a read-only query is better.
List<SubscriptionListItem> items = subscriptionQueryService.search(condition);
On the other hand, in use cases that cancel a subscription or change a plan, we should load the Aggregate and perform behavior.
Subscription subscription = subscriptionRepository.findById(id);
subscription.changePlan(targetPlan, policy, now);
subscriptionRepository.save(subscription);
Separating the concerns of reading and changing prevents Repository from becoming too large.
Where should the Repository interface be placed?
In many DDD implementations, the Repository interface is placed in the domain layer, and the implementation is placed in the infrastructure layer.
domain
Subscription
SubscriptionRepository
infrastructure
JpaSubscriptionRepository
The purpose of this structure is to prevent the domain model from directly depending on storage technology. The domain layer only knows the abstraction SubscriptionRepository; it does not need to know whether the actual implementation is JPA or MyBatis.
However, this principle does not need to be applied mechanically. If the project is small and the storage technology is simple, we can start with a simpler structure. What matters is whether the dependency direction of Repository contaminates the domain model.
If using JPA, Spring Data Repository can be used directly as a domain Repository. But if that interface becomes filled with technical query methods and starts providing update methods that bypass domain behavior, we should be careful.
Deletion can also be domain behavior
Repository usually has a delete method. But we should think about what deletion actually means in the domain.
Does deleting a subscription mean physically deleting a database row? Or does it mean canceling it? Does it mean removing a test subscription created by mistake by an operator? Does it mean anonymizing it after the personal information retention period has passed?
In the domain, state change is often more natural than simple deletion.
subscription.cancelByMember(reason, now);
subscription.expire(now);
subscription.terminateByAdmin(reason, now);
Physical deletion may be a technical action. Before expressing domain behavior as repository.delete(subscription), we should ask what that deletion means in the domain.
Closing
Repository is not a DAO. Repository is a domain-friendly abstraction for retrieving domain objects from storage and saving them again.
If Repository bypasses domain behavior and starts changing state directly, the Aggregate loses the chance to protect its rules. Conversely, if Repository handles object lifecycle around the Aggregate Root, domain behavior can remain inside domain objects.
The important question is not “What query should we create?” It is “How will the domain object come back to life, perform behavior, and then be saved?”
Repository is not decoration that hides the database. It is an entrance that lets the domain model survive in its own language without being pulled by the language of storage technology.