What Should Repositories Return in Domain-Driven Design?

Loading...
What Should Repositories Return in Domain-Driven Design?

A while ago, I did some research on repositories in Domain-Driven Design (DDD). One question kept coming up: what should repositories actually return?

It sounds simple, but the more I looked into it, the more I realized how much impact this decision has on the design of a system. Since then, I’ve formed my own perspective, and I think it’s worth sharing.

Why This Question Even Matters

Repositories sit right at the heart of a DDD project. They act as the bridge between the domain model and the persistence layer, so the domain logic doesn’t need to know anything about databases or APIs.

Because of this role, what a repository returns is not just an implementation detail. It shapes how the domain interacts with data and whether we keep a clean separation of concerns or let infrastructure concerns creep into our core logic.

The Common Options

When designing repositories, I usually see three approaches:

1. Entities or Aggregates

This is the classic approach. A repository that manages User aggregates should return a User object.

php
1 2 3 4 5
interface UserRepository
{
    public function findById(UserId $id): ?User;
    public function save(User $user): void;
}

This works well because repositories are conceptually like in-memory collections of aggregates. If you pull something out, you expect to get back an aggregate with all its behavior intact.

2. Data Transfer Objects (DTOs)

Sometimes teams make repositories return DTOs or even arrays:

php
1
public function findById(UserId $id): ?UserDto;

This may feel lightweight, but there’s a cost: DTOs don’t belong in the domain layer. They’re an application or infrastructure concern. If we start passing DTOs around inside the domain, we risk losing the very behaviors that make aggregates useful.

3. Primitives or Special Results

Not everything requires a full aggregate. Sometimes a repository just needs to answer a simple domain question, like whether a user exists:

php
1
public function exists(UserId $id): bool;

Or return a count:

php
1
public function countActiveUsers(): int;

As long as it’s part of the domain’s language, returning primitives is perfectly fine.

Tradeoffs and Practical Examples

Let’s say we’re working with a User aggregate. If we want to deactivate a user, we need the full aggregate:

php
1 2 3 4 5 6 7 8
$user = $userRepository->findById(new UserId('1234'));

if ($user === null) {
    throw new UserNotFound();
}

$user->deactivate(); // domain logic
$userRepository->save($user);

Here, returning a User aggregate makes sense because we need its behavior. But for something like checking username availability, a boolean is enough:

php
1 2 3
if ($userRepository->existsByUsername($username)) {
    throw new UsernameAlreadyTaken();
}

In this case, creating and returning an aggregate would be unnecessary.

My Perspective

After weighing these options in different projects, I’ve landed on a simple set of guidelines:

  • Return aggregates when the caller needs to use domain behavior.
  • Return primitives when they represent a valid domain-level answer (like exists or count).
  • Avoid returning DTOs directly from repositories - keep them in the application layer or use dedicated query services.

This way, repositories stay focused on serving the domain, and we keep a clean boundary between layers.

Conclusion

So, what should repositories return in DDD?

  • Aggregates, most of the time.
  • Primitives, when the domain question is simple.
  • Never infrastructure-specific objects like DTOs or arrays.

At the same time, it’s worth remembering there’s no single “correct” answer that fits every project. Different teams and practitioners interpret DDD in their own way, and that’s fine. What matters most is staying intentional: design repositories in a way that serves your domain model, not the other way around.

Loading...