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 faultSECURITY— a sign-in, a denied permission, a data exportERROR— 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-000000000000actor.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.