Search code examples
oopexceptionrepositorydomain-driven-designdesign-by-contract

Should a Repository throw an exception if no change is to be stored?


During the stipulation of the contracts of my Repositories I started to wonder about an essential contract of any Repository: What happens if Update is called with a an entity on which no change is to be stored?

  1. As a client of the Repository I would only call Update if I wanted to store a change. If there is no change, I would assume something went wrong inside my process so I want to be informed about this fact.

So I'd say: An uncommitted change is a precondition of the Update method. This entails throwing an Exception if this precondition is not met.

  1. But here is another view: The Repository is indifferent to change. The Update method just ensures that a given entity is persisted as it now is. If there was a change at all is not the Repository's business. So no Exception.

Personally, I tend to view 1 since the term Update itself suggests that a change happened.

What is your opinion to this topic?


PS: Let's suppose (for the sake of the example) that there is no concurrency involved which of course would lead to other possible outcomes.


Solution

  • What happens if Update is called with a an entity on which no change is to be stored?

    I think you want that to be a no-op.

    The usual pattern for working with a persistence backed repository looks something like

    final Cargo cargo = cargoRepository.find(trackingId);
    cargo.assignToRoute(itinerary);
    cargoRepository.store(cargo);
    

    This code lives in the application component; the repository and the Cargo aggregate root are the abstractions that the domain model is allowed to know about. The details of the implementation are somewhere else.

    Riddle: what happens if cargo.assignToRoute is, for the current state, a no-op? The application can't know this, because it doesn't have access to the underlying state. It invoked the model, the model decided not to change anything, and so the repository finds the same state that is available in storage.

    This can matter because, if your messages are coming to you over unreliable transport (like the web), you might get two copies of the same message. If the domain model recognizes that the new itinerary is equal to the old one, it doesn't need to change anything.

    final Cargo cargo = cargoRepository.find(trackingId);
    cargo.assignToRoute(itinerary);
    cargoRepository.store(cargo);
    final Cargo cargo = cargoRepository.find(trackingId);
    cargo.assignToRoute(itinerary);
    cargoRepository.store(cargo);
    

    Is introducing an exception in this flow actually delivering business value? or are we just inventing extra work for ourselves?

    Are we really putting in a bunch of extra scaffolding to detect the following, harmless, code at runtime?

    final Cargo cargo = cargoRepository.find(trackingId);
    cargoRepository.store(cargo);
    

    A way of thinking about the mechanics here: we don't call the model to make it do anything. We call the domain model because we want a particular post condition to be satisfied. If the model already satisfies that post condition, then there's no change to make.

    This pattern is typical of idempotent receivers; for example, the HTTP PUT method is reserved for operations that promise idempotent semantics.

    (More precisely, you are proposing a precondition -- that the caller ensure that the two states are different -- that must be satisfied to invoke the method. But in this case the precondition is a phantom; the method could satisfy the post condition, it just chooses not too.)