3

I'm learning DDD and I'm wondering if it's ok to invoke domain model logic from inside the repository layer? I don't mean that the business logic exists as part of the repository, merely that the repository calls the business logic of the domain model.

I've been reading a lot about DDD and it seems that the Service layer is usually the one that invokes a domain model's business logic. The repository is just a dumb storage abstraction that is only concerned with retrieving/persisting domain models. However if I need to perform an atomic update on a domain model without having to expose a Transaction to the service layer, then invoking the update from within the repository would be the only way.

So instead of:

// -----Service------
// some_model = repo.Get(...)
// some_model.Update(...)
// repo.Save(some_model)

Is it ok to just pass a changeset through to the repo and let the repo do the updating like so:

// ------Service------
// repo.Update(some_changeset)

// ------Repository------
// tx.Begin()
// some_model = db.GetByID(...)
// some_model.Apply(some_changeset)
// db.Save(some_model.Serialized)
// tx.Commit()

Forgive me if this is a simple question, but I haven't managed to find an answer to this anywhere.

I suspect the answer is 'yes' since the Repository resides in the bounded context of the Service layer, but I'd like to get some comments or confirmation if possible.

3
  • ... without having to expose a Transaction to the service layer Can you tell us why you do not want the repository to have methods like beginTransaction() and commitTransaction() ?
    – k3b
    Aug 30, 2019 at 9:24
  • 1
    My reason is that transactions seem to be an implementation detail of the store no? It shouldn’t be the service layer’s concern whether the database implementation is eventually consistent or ACID, should it? Aug 30, 2019 at 16:21
  • Also some databases implement transactions as objects with many more implementation-specific methods than just begin and commit. If I were to hide that behind some generic interface, a lot of functionality would be lost. I understand this to be the typical tradeoff for abstractions, but I’d want to avoid having to do that if possible. Worse comes to worse I will probably inject a function into the repository layer which the repo will then execute in a transaction, thereby avoiding the leaky abstraction problem. Aug 30, 2019 at 16:24

1 Answer 1

8

Is it acceptable to invoke business logic inside the repository layer?

The DDD police are not going to come and kick down your door.

But...

The motivation for the repository pattern is to "decouple application and domain design from persistence technology". So adding domain logic behind the repository interface rather defeats the purpose.

It also makes it more difficult to guess where the domain logic is. When all of the domain logic is in the domain model, the only guess is the right one. When some of the logic is instead in the repository implementation, or in the persistence store itself, the rediscovering the code that needs to change becomes more difficult.

However...

Is it ok to just pass a changeset through to the repo and let the repo do the updating like so...

You are really close to discovering a different pattern which can be effective, even though it isn't the one described by Evans.

If think carefully about the different elements, you may realize that the mutable state lives in the repository, rather than in the domain model. We talk about aggregates and entities, but those are just transient changes. The real object that changes is the repository itself.

This might suggest a design where the "domain logic" is built from pure functions, and the repository manages the mutable state.

Repository::onChange(id, domainLogic)
    oldValue = this.get(id)
    newValue = domainLogic(oldValue)
    this.replace(oldValue, newValue)

You end up with something like a monad - an opaque box that manages the mutable state for you.

The good news? A lot of the people who have been soaking in DDD for a long time assert that the important part of the Blue book is not the sections on implementation patterns. Texts like Domain Modeling Made Functional discard the "object oriented" focus altogether, and those designs seem to be healthy.

In any given project, I think you'll be best served to pick one style and stay with it, but it is possible that there have been improvements on what we considered to be "best practices" in Java over fifteen years ago.

Does this violate the single responsibility principle? The repository is now invoking the domain model’s business logic which means it is now responsible for more than just object storage. It now has knowledge of which business processes to run and has to handle business errors which may occur also

Not quite; the "domainLogic" argument is a function whose implementation is in the domain model, the persistence component doesn't know anything about it other than the signature. It's actually the application layer that is passing the function to the repository.

How about an example -- let's try one of the shipping use cases: assigning cargo to a route. Here's roughly what it looks like in the usual idiom

Application::assignCargoToRoute(itinerary, trackingId) {
    cargo = repository.getById(trackingId)
    cargo.assignToRoute(itinerary)
    repository.save(cargo)
}

Here's the alternative I described:

Application::assignCargoToRoute(itinerary, trackingId) {
    // Notice: this returns a _function_
    domainLogic = domainModel.assignToRoute(itinerary)
    repository.onChange(trackingId, domainLogic)
}

Note that the "responsibilities" really are separated:

  • the domain model knows about the logic, but doesn't know anything about orchestration or state
  • the application knows about orchestration, but knows nothing about the logic or the state
  • the repository knows about state, but nothing of orchestration or logic

Here's the underlying trick -- buried in the domain model is the potential for a pure function that looks something like:

Cargo.State assignCargoToRoute(trackingId, cargoState, itinerary)

In one case, we are "closing" over the cargoState, and accepting the other arguments later

Cargo {
    Cargo.State state;
    void assignToRoute(trackingId, itinerary) {
        this.state = PureFunction::assignCargoToRoute(trackingId, this.state, itinerary)
    }
}

In the other, we are closing over the command data, and deferring the state:

Function<Cargo.State, Cargo.State) assignoRoute(trackingId, itinerary) {
    return new Function<> (Cargo.State state) {
        return PureFunction::assignCargoToRoute(this.trackingId, state, this.itinerary);
    }
}

The second spelling is alien, but it's really equivalent to the first. All we've done is draw the responsibility boundaries slightly differently from how they were described fifteen years ago.

4
  • For the first part of the answer, my question is actually more superficial than that. My assumption is that all domain logic resides within the domain model. Aug 30, 2019 at 16:18
  • My question was actually about whether or not it’s considered code smell for the repository layer to be invoking the domain model’s business logic instead of having the service layer first retrieve the domain model from the repository, then executing the domain model’s business logic. Aug 30, 2019 at 16:18
  • I guess my question is really one of execution context rather than where domain logic should be placed, i.e - Is it ok for the repository to be the execution context rather than the service layer? Aug 30, 2019 at 16:19
  • The second segment of your answer is a very interesting approach that I hadn’t considered. I can see that here, the execution context is the repository layer and the service layer is not used to execute business logic… which goes to the heart of my question. Does this violate the single responsibility principle? The repository is now invoking the domain model’s business logic which means it is now responsible for more than just object storage. It now has knowledge of which business processes to run and has to handle business errors which may occur also. Aug 30, 2019 at 16:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.