Published on 02 June 2019 by @mathiasverraes
Embed more meaning in messages by using verbs.
Our industry has a tradition of unnatural, compact, lossy naming. This may save on typing, but costs in understandability.
Naming is hard, because we name things. If we understand the domain and have discussed with domain experts, we don’t have to invent names, we can use the names from that domain. And no domain experts says
Payment Event or
Invoice Paid or
OnAfterPayment, they say
The invoice was paid.
Use descriptive, natural language names. Do not remove the verbs. Use past tense sentences for events, imperative sentences for commands, questions for queries, negative statements for exceptions. (In English, I have a slight preference to leave of the article (“the”, “a”), but I might be convinced otherwise.)
Using natural language names helps us to see communication in our systems as mimicking human communication. This is how people tell stories: This has happened, that has happened, I did this, I expected that to happen. Embedding that language in our code and artefacts, is a core idea in Domain-Driven Design, because it enables two-way conversations. We may not expect a domain expert to actually read this code, but we can write it in a way that they would understand. Later, when more work needs to be done on the code, the intent is still very clear, and we can easily go abck and forth between code and domain.
We can test scenarios by structuring example stories:
scenario .given(new OrderWasPlaced(orderId=5, ...)) .when(new FulfilOrder(orderId=5)) .then(new UnpaidOrdersMayNotBeFulfilled()) // exception scenario .given(new OrderWasPlaced(orderId=5, ...)) .and(new InvoiceWasPaid(invoiceId=6, orderId=5, ...)) .when(new FulfilOrder(orderId=5)) .then(new OrderWasFulfilled(orderId=5))
This style of writing tests as example stories is common in Behaviour-Driven Development, often using a DSL called Gherkin. But in a messaging architecture, it can be written in plain code as illustrated above.
If we see a persona as a history, we can write reusable scenarios:
angryCustomerJim = scenario .given(new OrderWasPlaced(...)) .given(new InvoiceWasPaid(...)) .given(new ShipmentWasDelayed(...)) .given(new OrderWasPlaced(...)) .given(new InvoiceWasPaid(...)) .given(new ShipmentWasDelayed(...))
The actual test can now reuse this story:
scenario .involving(angryCustomerJim) .when(new ExpediteShipment(...)) .then(...)
The pattern for scenarios that change the state of the system is “given events, when command, then events”. The pattern for queries is “given events, when query, then response”.
scenario .given(new CustomerWasInvoiced(...)) .given(new CustomerWasInvoiced(...)) .given(new InvoiceWasPaid(...)) .given(new CustomerWasInvoiced(...)) .when(new HowManyInvoicesAreUnpaid()) .then(new NumberOfUnpaidInvoices(5))
The benefits are not limited to tests. By using well-chosen class and method names, intent becomes even more explicit.
class KeepingTrackOfUnpaidInvoices implements Projector when(CustomerWasInvoiced e) unpaidInvoices++ when(InvoiceWasPaid e) unpaidInvoices-- ask(HowManyInvoicesAreUnpaid q) return new NumberOfUnpaidInvoices(unpaidInvoices)
In F#, identifiers wrapped in three backticks can contain spaces and other characters.
'''Invoice was paid''' (This blog can’t print backticks, just imagine them.) This makes it even more natural. A problem arises when integrating with other systems, where those characters might not be supported.
Follow @mathiasverraes on Twitter.
This work by Mathias Verraes is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 License.