Search code examples
domain-driven-designddd-servicedddd

Query remote rest service from ddd aggregate


I've read about the Double Dispatch pattern, which enables to pass service interfaces into aggregate methods: https://lostechies.com/jimmybogard/2010/03/30/strengthening-your-domain-the-double-dispatch-pattern/, http://blog.jonathanoliver.com/dddd-double-dispatch/.

In my domain I have a BitbucketIntegration aggregate, which is local copy of a remote bitbucket account with some additional domain specific data. Now, I have to synchronize repositories and teams, etc.. from the cloud to be able to do business operations on them. In my first implementation I was using a service to access the Bitbucket Cloud, then set the aggregate's repositories, teams, account. This way I had a DDD mixed with Anemic Domain Model, since half of the aggregates state was set using setter-like methods from the service. With Double Dispatch I can pass e.g. a BitbucketService interface into method arguments. This way, the aggregate can protect it's invariants more, since some of the data can only be verified by connecting to the rest service (e.g. if the aggregate's accessToken, bitbucketAccount and repositories are in sync), which was the service's responsibility. One more thing that smells is that I have an accessToken field in my aggregate, which is only a technical concern.

Are there any recommended patterns for keeping a copy of a remote resource in a ddd aggregate? Also, how to keep the technical side out of it? Or was the first method with a domain service good enough?

Now the code looks something like:

class BitbucketIntegration extends Aggregate<UUID> {

    accountId: BitbucketId 
    repos: List<Repository>
    localData: ...
    // ... and more

    Single integrateWith(accessToken, queryService) {
        var id = queryService.getAccountAsync(accessToken);
        var repos = queryService.getReposAsync(accessToken);
        return Single.zip(id, repos, 
                (i, r) -> new BitbucketIntegratedEvent(accessToken, i, r))
            .onSubscribe(event -> apply(event))
    }

    Observable doSomeBusinessLocally(data) { ... return events; } 

    // this is triggered by a saga
    Single pollForChanges(queryService) {
        var dataFromRemote = queryService.synchronizeAsync(this.accessToken);
        ....
        return event;
    }
}

class CommandHandler {
    queryService: BitbucketService

    Completable handle(integrateCmd) {
        aggregate = repo.get(integrateCmd.id);
        return aggregate.integrateWith(integrateCmd.accessToken, queryService)
            .flatMap(event -> repo.store(event));
    }
}

As a side note, I only query Bitbucket.

EDIT: Martin Fowler writes about accessing an external system, including the definition of an Anti-Corruption Layer, which translates the remote resource representation to domain types.


Solution

  • If you inject infrastructure services into your Aggregate (by constructor or by method invocation) then you won't have a pure domain model anymore. This includes even services that have interfaces defined in the domain layer. It affects testability and introduces a dependency on the infrastructure. It also breaks the Single responsibility principle and it forces the Aggregate to know things it does not really need to.

    The solution to this is to call the service before and pass the result to the Aggregate's method (i.e. in the Application layer).