Skip to content

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 NextInvoiceNumber that calculates the next number from the InvoiceCreated events. Since it has no parameters, the Query only filters by Event Type, and the resulting AppendCondition fails if any invoice was created in the meantime:

model "Invoice number"

// Types
tag type InvoiceNumber = integer
type InvoiceData = object

// Events
event InvoiceCreated { invoiceNumber: InvoiceNumber, invoiceData: InvoiceData }

// Projections
projection NextInvoiceNumber: InvoiceNumber = 1 {
  on InvoiceCreated => set successor(event.data.invoiceNumber)
}

// Commands
command CreateInvoice(invoiceData: InvoiceData) {
  read next = NextInvoiceNumber()

  emit InvoiceCreated { invoiceNumber: next, invoiceData }

  scenario "Create first invoice" {
    when CreateInvoice { invoiceData: { foo: "bar" } }
    then InvoiceCreated { invoiceNumber: 1, invoiceData: { foo: "bar" } }
  }

  scenario "Create second invoice" {
    given InvoiceCreated { invoiceNumber: 1, invoiceData: { foo: "bar" } }
    when CreateInvoice { invoiceData: { bar: "baz" } }
    then InvoiceCreated { invoiceNumber: 2, invoiceData: { bar: "baz" } }
  }
}

CreateInvoice reads 1 type, no tag

Query ItemEvent TypesTags
nextInvoiceCreatednone

AppendCondition: failIfEventsMatch the Query above, after the position of the last Event read

Tags of the appended events: InvoiceNumber:{next}

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

Conclusion

This example demonstrates how a DCB compliant Event Store can simplify the creation of monotonic sequences