Ivan Zyranau
Ivan Zyranau

Reputation: 941

How to query multiple aggregates efficiently with DDD?

When I need to invoke some business method, I need to get all aggregate roots related to the operation, even if the operation is as primitive as the one given below (just adding item into a collection). What am I missing? Or is CRUD-based approach where you run one single query including table joins, selects and insert at the end - and database engine does all the work for you - actually better in terms of performance?

In the code below I need to query separate aggregate root (which creates another database connection and sends another select query). In real world applications I have been querying a lot more than one single aggregate, up to 8 for a single business action. How can I improve performance/query overhead?

Domain aggregate roots:

class Device
{
    Set<ParameterId> parameters;

    void AddParameter(Parameter parameter)
    {
        parameters.Add(parameter.Id);
    }
}

class Parameter
{
    ParameterId Id { get; }
}

Application layer:

class DeviceApplication
{
    private DeviceRepository _deviceRepo;
    private ParameterRepository _parameterRepo;

    void AddParameterToDevice(string deviceId, string parameterId)
    {
        var aParameterId = new ParameterId(parameterId);
        var aDeviceId = new DeviceId(deviceId);

        var parameter = _parameterRepo.FindById(aParameterId);
        if (parameter == null) throw;

        var device = _deviceRepo.FindById(aDeviceId);
        if (device == null) throw;

        device.AddParameter(parameter);
        _deviceRepo.Save(device);
    }
}

Possible solution

I've been told that you can pass just an Id of another aggregate like this:

class Device
{
    void AddParameter(ParameterId parameterId)
    {
        parameters.Add(parameterId);
    }
}

But IMO it breaks incapsulation (by explicitely emphasizing term ID into the business), also it doesn't prevent from pasting wrong or otherwise incorrect identity (created by user).

And Vaughn Vernon gives examples of application services that use the first approach (passing whole aggregate instance).

Upvotes: 2

Views: 4547

Answers (2)

Alexey Zimarev
Alexey Zimarev

Reputation: 19630

If you figured out that having RDBMS query with joins will work in this case - probably you have wrong aggregate boundaries.

For example - why would you need to load the Parameter in order to add it to the Device? You already have the identity of this Parameter, all you need to do is to add this id to the list of references Parameters in the Device. If you do it in order to satisfy your ORM - you're most probably doing something wrong.

Also remember that your aggregate is the transactional boundary. You really would want to complete all database operations inside one transaction and one connection.

Upvotes: 1

Codescribler
Codescribler

Reputation: 2305

The short answer is - don't query your aggregates at all.

An aggregate is a model that exposes behaviour, not data. Generally, it is considered a code smell to have getters on aggregates (ID is the exception). This makes querying a little tricky.

Broadly speaking there are 2 related ways to go about solving this. There are probably more but at least these don't break the encapsulation.

Option 1: Use domain events - By getting your domain (aggregate roots) to emit events which illustrate the changes to internal state you can build up tables in your database specifically designed for querying. Done right you will have highly performant, denormalised queryable data, which can be linearly scaled if necessary. This makes for very simple queries. I have an example of this on this blog post: How to Build a Master-Details View when using CQRS and Event Sourcing

Option 2: Infer query tables - I'm a huge fan of option 1 but if you don't have an event sourced approach you will still need to persist the state of your aggregates at some point. There are all sorts of ways to do this but you could plug into the persistence pipeline for your aggregates a process whereby you extract queryable data into a read model for use with your queries.

I hope that makes sense.

Upvotes: 4

Related Questions