Invoice number
Creating a monotonic sequence without gaps is another common requirement DCB can help with
Challenge
Create invoices with unique numbers that form an unbroken sequence
Traditional approaches
As this challenge is similar to the Unique username example, the traditional approaches are the same.
DCB approach
This requirement could be solved with an in-memory Projection that calculates the nextInvoiceNumber
:
Info
This example uses composed projections to build Decision Models (explore library source code )
// event type definitions:
function InvoiceCreated({ invoiceNumber, invoiceData }) {
return {
type: "InvoiceCreated",
data: { invoiceNumber, invoiceData },
tags: [`invoice:${invoiceNumber}`],
}
}
// projections for decision models:
function NextInvoiceNumberProjection(value) {
return createProjection({
initialState: 1,
handlers: {
InvoiceCreated: (state, event) => event.data.invoiceNumber + 1,
},
})
}
// command handlers:
class Api {
eventStore
constructor(eventStore) {
this.eventStore = eventStore
}
createInvoice(command) {
const { state, appendCondition } = buildDecisionModel(this.eventStore, {
nextInvoiceNumber: NextInvoiceNumberProjection(),
})
this.eventStore.append(
InvoiceCreated({
invoiceNumber: state.nextInvoiceNumber,
invoiceData: command.invoiceData,
}),
appendCondition
)
}
}
// test cases:
const eventStore = new InMemoryDcbEventStore()
const api = new Api(eventStore)
runTests(api, eventStore, [
{
description: "Create first invoice",
when: {
command: {
type: "createInvoice",
data: {"invoiceData":{"foo":"bar"}},
}
},
then: {
expectedEvent: InvoiceCreated({"invoiceNumber":1,"invoiceData":{"foo":"bar"}}),
}
},
{
description: "Create second invoice",
given: {
events: [
InvoiceCreated({"invoiceNumber":1,"invoiceData":{"foo":"bar"}}),
],
},
when: {
command: {
type: "createInvoice",
data: {"invoiceData":{"bar":"baz"}},
}
},
then: {
expectedEvent: InvoiceCreated({"invoiceNumber":2,"invoiceData":{"bar":"baz"}}),
}
},
])
Info
This example uses composed projections to build Decision Models (explore library source code )
// event type definitions:
function InvoiceCreated({
invoiceNumber,
invoiceData,
} : {
invoiceNumber: number,
invoiceData: { },
}) {
return {
type: "InvoiceCreated" as const,
data: { invoiceNumber, invoiceData },
tags: [`invoice:${invoiceNumber}`],
}
}
type EventTypes = ReturnType<typeof InvoiceCreated>
// projections for decision models:
function NextInvoiceNumberProjection() {
return createProjection<EventTypes, number>({
initialState: 1,
handlers: {
InvoiceCreated: (state, event) => event.data.invoiceNumber + 1,
},
})
}
// command handlers:
class Api {
constructor(private eventStore: EventStore) {}
createInvoice(command: { invoiceData: { } }) {
const { state, appendCondition } = buildDecisionModel(this.eventStore, {
nextInvoiceNumber: NextInvoiceNumberProjection(),
})
this.eventStore.append(
InvoiceCreated({
invoiceNumber: state.nextInvoiceNumber,
invoiceData: command.invoiceData,
}),
appendCondition
)
}
}
Experimental: 3rd party library
These Given/When/Then scenarios are visualized using an unofficial, work-in-progress, library
Better performance
With this approach, every past InvoiceCreated
event must be loaded just to determine the next invoice number. And although this may not introduce significant performance concerns with hundreds or even thousands of invoices — depending on how fast the underlying Event Store is — it remains a suboptimal and inefficient design choice.
Snapshots
One workaround would be to use a Snapshot to reduce the number of Events to load but this increases complexity and adds new infrastructure requirements.
Only load a single Event
Some DCB compliant Event Stores support returning only the last matching Event for a given QueryItem
, such that the projection could be rewritten like this:
function NextInvoiceNumberProjection(value) {
return createProjection({
initialState: 1,
handlers: {
InvoiceCreated: (state, event) => event.data.invoiceNumber + 1,
},
onlyLastEvent: true,
})
}
Alternatively, for this specific scenario, the last InvoiceCreated
Event can be loaded "manually":
// event type definitions:
function InvoiceCreated({ invoiceNumber, invoiceData }) {
return {
type: "InvoiceCreated",
data: { invoiceNumber, invoiceData },
tags: [`invoice:${invoiceNumber}`],
}
}
// projections for decision models:
function NextInvoiceNumberProjection(value) {
return createProjection({
initialState: 1,
handlers: {
InvoiceCreated: (state, event) => event.data.invoiceNumber + 1,
},
})
}
// command handlers:
class Api {
eventStore
constructor(eventStore) {
this.eventStore = eventStore
}
createInvoice(command) {
const projection = NextInvoiceNumberProjection()
const lastInvoiceCreatedEvent = this.eventStore
.read(projection.query, {
backwards: true,
limit: 1,
})
.first()
const nextInvoiceNumber = lastInvoiceCreatedEvent
? projection.apply(
projection.initialState,
lastInvoiceCreatedEvent
)
: projection.initialState
const appendCondition = {
failIfEventsMatch: projection.query,
after: lastInvoiceCreatedEvent?.position,
}
this.eventStore.append(
new InvoiceCreated({
invoiceNumber: nextInvoiceNumber,
invoiceData: command.invoiceData,
}),
appendCondition
)
}
}
const eventStore = new InMemoryDcbEventStore()
const api = new Api(eventStore)
api.createInvoice({invoiceData: {foo: "bar"}})
console.log(eventStore.read(queryAll()).first())
Conclusion
This example demonstrates how a DCB compliant Event Store can simplify the creation of monotonic sequences