Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

From CRUD to DDD

I've got an application that manages tickets and customers. A customer owns many tickets. If a customer is deleted, so are its tickets. This is one of the tests to see if an object should be an aggregate or not. I chose that they should both be entities not under an aggregate root. I will commonly load and display a list of tickets spanning many customers. I am using SQL Server to save the data in a relational format.

My architecture is the following:

  1. ASP.NET MVC application.
  2. WCF service(Facade). This service has public methods such as GetTicket, GetTickets, GetCustomer, and GetCustomers. It uses repositories directly for this. It also has a public method called Run. Run accepts a command, which can be AssignTicket, ChangeTicketName, etc. To handle the commands, the facade is injected with services, ITicketService and ICustomerService. Methods on these services are used to execute the commands from the facade. The services use ITicketRepository and ICustomerRepository to load data.
  3. Relation SQL Server database

I'm getting hung up on maintaining references from Tickets to Customers. Relationally, a Ticket has a FK that maps to the Customers table. When the UI views an individual Ticket, it calls Facade.GetTicket(id) and Facade.GetCustomers(). I'm able to display all of the ticket details and the customer's name that it belongs to. That's all fine and dandy. But what about a big list of tickets that are owned by many different customers. Remember, the Ticket only holds a Guid that references the customer. I don't want to have to call the facade to get the customer's name for every single ticket that I'm going to list in the UI. Is it valid for my Tickets to have a value object called CustomerInfo( CustomerId and CustomerName ) on them? Is it valid to use an sql statement to join this data in? What if I weren't using a relational db? What about when a customer's name is changed? In the system, Ticket's actually hold references to multiple other entities as well.

It seems that there is a disconnect in the DDD business logic and what the UI needs to display.

like image 764
Darthg8r Avatar asked Dec 03 '22 21:12

Darthg8r


1 Answers

If a customer is deleted, so are its tickets. This is one of the tests to see if an object should be an aggregate or not.

I wouldn't expect your domain experts to ever say they "delete customers". Customers may be barred, or their Account suspended, Tickets may be Cancelled or Transferred, but the very term "delete" is very CRUD-centric. Ideally you shouldn't really ever hard-delete data (you could archive it to a different data store though, perhaps?), and IMHO cascading deletes is a dangerous move. Let your ORM deal with that.

Defining an Aggregate is about defining a "consistency boundary", within which the state of the aggregate is "correct" according to the invariants defined for that aggregate by the domain experts. That is how you define the Aggregate boundary.

I'm getting hung up on maintaining references from Tickets to Customers

Be clear that there's a difference between a Domain Model, wherein Entities and Value Objects are assembled into Aggregates, and a Data Model, which is really just a code representation of your database in your chosen OOP language. Your Data Model is your tables and could be a totally different shape to your Domain Model. Your Data Model can have relations between tables where no actual reference exists. In other words, you can have foreign key references across aggregate boundaries. This is only to enforce referential integrity in the database. If you were to use an ORM to map those tables to classes, you would not have "navigation properties", only the ID values.

It seems that there is a disconnect in the DDD business logic and what the UI needs to display.

Now we begin to talk about CQRS. DDD is about modelling the behaviour in your problem domain. A domain model should have lots of behaviour (and potentially no public state, which I'll come on to). When invoking domain logic, you should, in one transaction, load the aggregate, invoke the required behaviour and save the result. Your repository should therefore look something like this:

public interface IRepository<TEntity>
{
    TEntity Get(Guid id);
    void Save(TEntity item);
}

The implementation will always eagerly-load everything, because in your ORM mapping you won't have included references to entities outside the aggregate. In fact, you might now already be thinking that a document database is a better fit for storing aggregates, and IMHO you'd be right.

But how do you then query? Easy. Write a query. If you're implementing CQRS, you have a Thin Read Layer that DOES NOT use your repository, and DOES NOT use your carefully-crafted entities and their ORM mappings. Just write a query. If you're using SQL Server, consider knocking together a super-quick Linq to SQL data context, or use Dapper or Simple.Data. Or even just ADO.NET. The point is your queries are about getting some data out of a database, and you don't need behaviour for that.

If you're using a document database for your aggregate persistence, then a sensible implementation of the CQRS pattern could see you possibly building a totally separate database using events generated from the domain behaviour. Options for that "Read Store" are endless. Here are some ideas:

  • Same or other instance of document db but with pre-built viewmodels.
  • SQL database (you can give up 3rd-normal form and go with duplication since you want to optimise for read performance not OLTP performance).
  • Pre-written HTML files on disk?

Bonus Chatter (Event Sourcing)

Assuming you go for the approach of using events from your domain to build a Read Store that you access with a Thin Read Layer in your app (don't bother with repositories, or massive ORM frameworks. Use a bit of Dapper or something simple), how do you make sure you always have the right events with the right data in them to do so? Well, what if you didn't actually ever store the aggregates themselves? What if you stored the events emitted by the aggregates in an Event Store, with an Event Stream per aggregate? When you want to load an aggregate, you load it's events and replay them in memory to build up to the latest state of the aggregate.

I won't go into that in any more detail, but it's worth investigating CQRS and Event Sourcing as they're great pals with DDD and play very well together. :)

like image 81
Neil Barnwell Avatar answered Jan 01 '23 12:01

Neil Barnwell