Back to Blog
Infographic contrasting DAO's direct table access with a Repository's Aggregate Root and connected entities.
Engineering
Sep 19, 2023
5 Min Read

Is Your Repository Just a DAO in a Costume?

Earlier, we built a TaskDAO that worked as a single class with a clear role, hiding database operations behind a few methods. That seemed fine at first. But if you look closer, you’ll notice it doesn’t stop you from calling dao.save(TaskDTO(id=1, title="Ship it", completed=True)) twice in a row. The second call will mark a task as completed even if it already was, and the DAO just goes ahead with it. This isn’t technically wrong, since the DAO doesn’t enforce any rules about it.

Illustration of a DAO silently accepting the same task-completion call twice, with no rule stopping the duplicate.

This is where the Repository pattern, as defined in Domain-Driven Design, starts to show that it’s not just a “DAO with a nicer name.”

The small comparison

A DAO uses a storage technology, usually one per table, and returns simple records. For example, a TaskDTO just holds fields and doesn’t care what a ‘completed’ task means. Martin Fowler’s original idea of a Repository looks similar: it’s an interface that acts like a collection between your domain and the data-mapping layer. The structure is similar, but the rules are not.

Eric Evans made an important rule when he introduced the pattern: a Repository should only be used with an Aggregate Root, not with a table. Microsoft’s guidance on Domain-Driven Design is clear about this too: you should never make a repository for every table in your database. There should be one Repository per Aggregate Root, and all other rules follow from that.

An Aggregate Root is the object that controls a consistency boundary. It’s the main entity in a group of related data and decides if a change is valid. For example, a simple Task can be its own root because it’s straightforward. An Order with its line items, discounts, and shipping address is also a root, and its Repository might access three tables with one get() call. If you try to do this with DAOs, handling one table at a time, you have to manually coordinate three DAOs and hope none are missed.

Diagram comparing one Repository call spanning an Order aggregate against three separately coordinated DAOs.

What changes in the code

In practice, the change consists of behaviour moving from the caller to the object.

class TaskAlreadyCompletedError(Exception):
    pass


class Task:
    def __init__(self, id: int, title: str):
        self.id = id
        self.title = title
        self._completed = False

    def complete(self) -> None:
        if self._completed:
            raise TaskAlreadyCompletedError(f"Task {self.id} is already completed")
        self._completed = True

    @property
    def is_completed(self) -> bool:
        return self._completed

The Task above is no longer just a dataclass. Now, it’s an object that can make decisions. Instead of just setting completed to True, you ask the task to complete(), and it can say no. This means the Repository’s role becomes smaller and more focused, like this:

from abc import ABC, abstractmethod
from typing import Optional


class TaskRepository(ABC):
    @abstractmethod
    def add(self, task: Task) -> None: ...

    @abstractmethod
    def get(self, task_id: int) -> Optional[Task]: ...


class FakeTaskRepository(TaskRepository):
    def __init__(self):
        self._tasks: dict[int, Task] = {}

    def add(self, task: Task) -> None:
        self._tasks[task.id] = task

    def get(self, task_id: int) -> Optional[Task]:
        return self._tasks.get(task_id)

There are just two approaches here, instead of the six or seven methods that a growing DAO often ends up with. If you see methods like find_by_status or find_overdue, that’s a sign you’re slipping back into DAO habits. In DDD, you would use a separate query object for that, not add more methods to the Repository. A real implementation would still use Postgres or SQLite like TaskDAOSqlite did before. The key difference is that the domain logic, like refusing to complete a task twice, is now on the Task itself, not scattered among DAO callers.

There’s also a real difference in how writes are committed. With TaskDAOSqlite.save(), it called self.conn.commit() right in the method. That worked until you needed to change two Aggregates at once, like reassigning a task from one owner to another. Usually, a Repository works with something called a Unit of Work, which keeps the transaction open for all changes in one operation and then commits everything at the end, or not at all.

Sequence diagram contrasting per-call DAO commits with a single Unit of Work commit spanning two Aggregate changes.

Is it worth building

It’s not always necessary to use the Repository pattern. In fact, the biggest supporters of the pattern often warn about how teams actually use it. A ‘generic’ repository with lots of query methods and a simple update() that just saves whatever you give it isn’t really a Repository in Domain-Driven Design. It’s just a DAO pretending to be a domain model, with all the formalities but none of the real benefits.

The real question here isn’t whether you’re using an ORM, like with DAOs. Instead, you should ask if your domain model has important rules that need protecting, or if it’s just a bunch of fields being updated by forms. If it’s just fields and CRUD operations, you don’t need a Repository. A DAO or the ORM can do the job with less setup.

It’s also important to see where the Repository fits in. It’s meant for writing, loading, and saving whole Aggregates. If you’re making a report or a list view, there’s no need to use the Repository just to read one field from a Task. Those reads usually go straight to the database, without involving an Aggregate. So, the DAO never really went away; it just handles the reads now, while the Repository takes care of the writes.

Decision flowchart for choosing between a Repository and a DAO based on whether the domain model has real invariants, plus the read/write split.

Join the Conversation

This dispatch is part of an ongoing series on the future of intelligence. Share your perspective or subscribe for more.

Weekly dispatches. No spam. Ever.