Greg Young on a solution to CRUD
Greg Young gave a talk at QCon last year entitled Unshackle Your Domain. In it, he offers a solution to CRAPpy -- sorry I mean CRUDdy -- services.
CRUD services expose a set of objects. A client gets an object (Read), makes changes to it, and puts it back (Update, or Alter). One big problem with CRUD is that it is not auditable. There is no record of the change. Sure, you can keep a parallel audit history, but if your system does not rely upon that history, how can you trust it?
Greg points out that accountants don't make changes to objects. They keep histories. They never Update (Alter) records, and they never Delete (Purge). Their work is completely auditable. Their legers have a running total, but that current state is always based on history. Changes, not objects, are the foundation of their domain.
Record changes, not state
Greg proposes that a service can be modeled as a series of changes, rather than a collection of objects. In many domains, the set of changes is just as important as the set of objects. This is the only way to truly trust your audit log.
State transitions are an important part of our problem space and should be modeled within our domain.
- Greg Young
In addition to making a system auditable, this solves a whole host of validation and consistency problems. For example, in a CRUD service, we would model a customer as:
class Customer { identity CustomerId; string FirstName; string LastName; string City; string State; }
As a domain object, it would have validation. For example, the City must be within the State. But what if the customer moves from Dallas, Texas to Tulsa, Oklahoma? There is no method to get us there in one step. We either have to transition through Tulsa, Texas or Dallas, Oklahoma. We have to allow our object to be in an invalid state.
More subtle is the FirstName/LastName pair. There is no validation here, but it is just as incorrect to change one without the other.
Now consider the CRUD operations:
CreateCustomer(Customer c); UpdateCustomer(Customer c); DeleteCustomer(Customer c); List<Customer> GetCustomersByCity(string city, string state);
Suppose one client gets a customer and changes their name. At the same time, another client gets the same customer and changes their address. How does the server reconcile these two changes? There should be no conflict: the two clients performed two independent operations. But because we funnel all of the changes through UpdateCustomer, we cannot easily separate them.
A change-oriented service model
Greg proposes that the service should model the changes as first-class citizens. For example:
change AddCustomer
{
identity CustomerId;
string FirstName;
string LastName;
string City;
string State;
}
change RenameCustomer
{
identity CustomerId;
string FirstName;
string LastName;
}
change MoveCustomer
{
identity CustomerId;
string City;
string State;
}
change RemoveCustomer
{
identity CustomerId;
}
Each of these changes is a command object that is routed through the system. They are stored in a sequential log on the server. The current state of a customer is driven exclusively from this set of command objects. Since there is no other way to change a customer, the audit log can be trusted.
Command/query separation
A command changes the state of the system, and a query examines it.
Queries tend to be synchronous, since the server returns results. Some patterns such as AJAX perform queries asynchronously by providing a callback method to capture the results. Even so, the client is in a waiting state until the results are delivered, so the query is synchronous in essence if not in actual fact.
Commands tend to be asynchronous, since the server returns no results. The client only needs a guarantee that the server has received the command. Message queues are often used for commands.
A query can be retried with no ill effects. But this is not always true with a command. if a command is not idempotent, then we must ensure that it is executed once and only once.
Queries and commands are two very different things. Because they have different requirements, they should be handled with separate mechanisms. RPC-based systems, unfortunately, provide onlyl one mechanism for all messaging. There is no enforcement that queries have no side effects, or that commands return no results. As a consequence, queries and commands tend to be intermingled within a single RPC.
Greg proposes a different architecture. When commands are treated as first-class citizens, the separation between commands and queries is easy. Commands are sent to an audit log, and queries are processed out of a reporting database. Don't let the term reporting database confuse you; to Greg any query is a report.
Eventual consistency
The client workstation, the audit log, and the reporting database are three separate bounded contexts. Each has its own storage schema, its own transaction boundary, and its own interface. The three environments are isolated from one another. The only way for them to reach an agreement is for them to communicate.
Communication takes time. We must allow our distributed systems to be out-of-sync for that time. We cannot guarantee consistency across these bounded contexts at all times. The best we can do is to guarantee that they will become consistent eventually.
Most bounded contexts can interact with relaxed consistency.
- Greg Young
It will take time for data to flow through the system. Sometimes that time is not important. But sometimes decisions must be made with the most up-to-date data possible. Only a domain expert can tell us the difference.
When we acknowledge the fact that communication takes time, we open up a whole new set of discussions with the domain experts. Now we can ask how long is too long. We can start defining service level agreements at the command level. And we can start measuring those times to see when the SLA is violated.
Don't take consistency as a given. Don't automatically define a CRUD service. Take the time to model the changes in your domain. Make those changes part of your discussion with the domain experts. Make them part of your solution.