Messaging Patterns: Natural Language Message Names

Embed more meaning in messages by using verbs.

By Mathias Verraes
Published on 02 June 2019



Problem

Our industry has a tradition of unnatural, compact, lossy naming. This may save on typing, but costs in understandability.

Solution

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.)

Examples

Events: CustomerWasInvoiced, InvoiceWasPaid Commands: InvoiceCustomer, FulfilOrder Queries: WhichInvoicesAreUnpaid, HowManyInvoicesAreUnpaid Responses: UnpaidInvoices, NumberOfUnpaidInvoices Exceptions: UnpaidOrdersMayNotBeFulfilled

Discussion

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.

Test scenarios

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))

Code

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)

Going further

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.