DependencyInjection-2nd-edition / codesamples

Sample code for Dependency Injection Principles, Practices, and Patterns
https://manning.com/seemann2
MIT License
142 stars 58 forks source link

Delete ProductInventory when deleting Product #7

Open max-ch9i opened 3 years ago

max-ch9i commented 3 years ago

Dear @ploeh and @dotnetjunkie,

Suppose there is a new use case stating that when a Product is deleted, the ProductInventory with the Id of the deleted Product must be deleted as well. What is the best place to tie the delete operation on the Product to the delete operation on the ProductInventory?

The natural place to consider is DeleteProductService. Below IInventoryRepository's new method Delete deletes entries in ProductInventory before the Product is deleted from ProductRepository.

public DeleteProductService(
    IProductRepository productRepository, IInventoryRepository inventoryRepository)
{
    // Guards...

    this.productRepository = productRepository;
    this.inventoryRepository = inventoryRepository;
}

public void Execute(DeleteProduct command)
{
    // New method
    this.inventoryRepository.Delete(command.ProductId);
    this.productRepository.Delete(command.ProductId);
}

As a result DeleteProductService gains a new dependency - IInventoryRepository.

But could SqlProductRepository be a more appropriate place for this operation? If a Product is deleted, but ProductInventory still references the deleted Product's id, the Data Access Layer will be in an invalid state. The updated Delete method shows a possible implementation.

public void Delete(Guid id)
{
    this.context.ProductInventories.Remove(this.GetProductInventoryById(id));
    this.context.Products.Remove(this.GetById(id));
}

The number of dependencies for SqlProductRepository remains unchanged.

Thank you,

Max

dotnetjunkie commented 3 years ago

I would probably make the deletion of the inventory part of the product repository in case this 'cascading delete' is a hard rule; in other words deleting a product will always cause deletion of the inventory. This is probably the most obvious scenario, because referential integrity would prevent you from deleting the product from your RDBMS when its inventory records are still there.

On the other hand, if that relationship is not that strict, and deletion of inventory is not necessarily a precondition of removal of a product, implementing the deletion as part of the business logic might make more sense.

I could even imagine that both inventory management and product management each become part of a different bounded context, and part of a different service (possibly getting notified by each other through domain events through a durable queue, see section 6.1.3, page 179). When that's the case, both services will have their own code base and database. This will cause the deletion of the product and its inventory to live in completely different places.

ploeh commented 3 years ago

I concur 👍

It's part of the software architect's responsibility to decide where to place business logic. If you decide to model all data in a single fully normalised relational database with referential integrity, you'll implicitly also be putting significant business logic in the database, as @dotnetjunkie implies.

There can be good reasons to do that, so that's not a value judgment. If you do that, though, you're going to need a book about relational database design and maintenance. And that's not DIPPP 😉

Our book is about DI, indeed, but also object-oriented programming and design. In that context, we emphasise putting business logic in code rather than in databases. That's a trade-off. A valid criticism is that you end up replicating features already in the RDBMS. That's a fair criticism. In my experience, it makes sense to pull business logic into code when you anticipate that it has a certain degree of complexity.

On the other hand, if you're developing a system that's mostly CRUD it may be a better decision to put as many rules as possible into the database itself.

All that said, if you decide to put the business logic into the code, and if you decide to follow the Dependency Inversion Principle, you should implement the business rules and try to forget about the underlying storage system. As Steven implies, you might even have a situation where inventory management and product catalogue are two separate bounded contexts, each with their own database. In that situation, I would reach for eventual consistency:

  1. Put the most important Command on a durable queue. Let's say that's the Command to delete the product.
  2. Handle the Command in a background process. Make sure that the following steps are idempotent:
    1. Delete the product.
    2. Publish an event informing any subscribers that the product was deleted.
  3. Have another subscriber handle the product deleted event.

Anything between this kind of architecture, and a SQL transaction with cascading deletes is possible, depending on trade-offs.

max-ch9i commented 3 years ago

Thank you for sharing your thoughts.

It seems that putting business logic into the data access layer is less suitable solution more often than not, although not invalid. There may be good reasons to do so, but for complex applications keeping business rules in the domain layer is a good choice.

Evolving the application into having a bounded context for each of Product and ProductInventory would take less effort if the business logic is kept in the code and the DIP is followed.

Good shout to use a messaging queue for this use case!

Best wishes, Max