Skip to content

Emit a business event

This guide addresses module authors. It describes the envelope to publish, the naming convention of types, and how to emit one from a Java module.

The principle

A module that has just written something notable publishes an envelope on the logs topic of Apache Pulsar. It does not address the Event journal module, does not wait for it, and does not know what it does with the envelope. The journal consumes that topic on its own account.

An event describes an accomplished fact. It is therefore emitted after the write, never before, and its verb is in the past tense.

The envelope

The envelope is a JSON object:

{
  "specversion": "1",
  "id": "6f1c8e2a-9b34-4d51-8a7e-2c3f5b1d0e77",
  "time": "2026-08-23T09:41:12Z",
  "source": "stocks",
  "type": "stocks.inventory.validated",
  "organization_id": "c81b0d44-2f5a-4a19-9d7c-8e0a6b3f21ce",
  "actor": {
    "id": "3a7e1f90-5c26-4b83-a1d4-9f0e7c2b6d15",
    "name": "Awa Koné"
  },
  "subject": {
    "type": "inventory",
    "id": "1d9c4b7e-8a05-4f62-b3e1-7c25a0f9d834",
    "label": "INV-2026-0042"
  },
  "level": "INFO",
  "correlation_id": "b2f4a681-3d70-49ce-8b52-1a6e0c94f7d3",
  "data": {
    "warehouse": "Main warehouse",
    "lines": 17
  }
}
Field Required Role
specversion Yes Version of the envelope, "1"
id Yes UUID of the event, used for deduplication
time Yes Instant of the fact, ISO 8601 in UTC
source Yes Emitting module, identical to the first segment of type
type Yes Nature of the fact, per the convention below
organization_id Yes Organization the fact belongs to
actor Yes Author of the fact: its identifier and its name
subject Yes Entity concerned: its type, its identifier and its label
level Yes INFO, WARN, SECURITY or ERROR
correlation_id No Ties several events issued from the same processing
data Yes Free business data, a JSON object of 16 KiB at most

The whole envelope does not exceed 32 KiB, of which 16 KiB at most for data. Beyond that, the message goes to a dead letter rather than being truncated.

Labels are denormalized at emission

subject.label and actor.name are copied into the envelope at emission time, and never read again. An event therefore states what the label was worth on that day: renaming an item later does not rewrite history. This is intended.

The naming convention

A type reads <source>.<entity>.<past-tense-verb>, in lowercase ASCII:

[a-z0-9_]+(\.[a-z0-9_]+){2}

Exactly three segments, separated by dots. The first is the module name and must equal the source field. The second names the entity in the singular. The third is the verb, as a past participle.

The recommended verbs, to be preferred over any invention:

created, updated, deleted, validated, cancelled, executed, closed, archived, imported, exported

Correct Incorrect Why
stocks.inventory.validated stocks.validate_inventory Two segments, and an infinitive verb
sales.cash_receipt.closed sales.cashReceipt.closed camelCase does not match the pattern
stocks.article.updated stock.article.updated The source is the stocks module

The level

INFO is the default case. One departs from it only for a reason:

  • WARN — a fact that deserves attention without being a fault
  • SECURITY — a sign-in, a denied permission, a data export
  • ERROR — a notable business failure

The system actor

An event emitted outside an HTTP request — scheduled processing, message consumption, migration — has no user behind it. It then uses the system actor:

  • actor.id: 00000000-0000-0000-0000-000000000000
  • actor.name: the name of the module

In Java

The com.minlessika.erp:business-events library, package com.minlessika.events, carries the envelope and its publication.

final Envelope envelope = new Envelope(
    new Occurrence(
        new Stamp(),
        new EventType("stocks.inventory.validated"),
        orgId
    ),
    new Actor(userId, "Awa Koné"),
    new Subject("inventory", id, "INV-2026-0042"),
    new Details(Level.INFO, data)
);
broadcast.emitLater(new BusinessEventMessage(envelope));

Stamp sets the identifier and the instant of the event. emitLater defers the publication: the message is flushed after the commit of the transaction, and purged if it is rolled back. A fact that did not happen is therefore never journaled.

The call goes after the business write, in the same transaction as it.

Every module registers LogsPublishing once, at startup, to obtain broadcast.

Once emitted

The Event journal module consumes the logs topic and stores the envelopes in an events table partitioned by month. Deduplication is done on id: republishing the same envelope twice does not create two rows.

An envelope the consumer cannot read goes to logs-DLQ and becomes visible in the Dead letters screen. A type outside the pattern or an oversized envelope end up there: that is where to look when an expected event does not appear in the journal.

Finally, remember to declare your new event in the Event catalog.

Changelog

  • 1.0 (August 23, 2026): document created.