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