A semantic identifier for occurrences: CloudEvent "type"

In today's xRegistry WG Friday meeting we had a debate about the relationship between the message identifier (messageid) that uniquely identifies a metadata template for a CloudEvent (if that is the chosen envelope format) and the CloudEvent type attribute.

In the message Registry spec we recommend for those two identifiers to be the same; the example below shows this:

"Contoso.MotorVehicle.OilTemperatureThresholdExceeded": {
    "description": "Alarm event for when the oil temperature exceeds a threshold",
    "envelope": "CloudEvents/1.0",
    "envelopemetadata": {
        "id": { "required": true },
        "type": {
            "value": "Contoso.MotorVehicle.OilTemperatureThresholdExceeded",
            "description": "Event raised when oil temperature exceeds a set threshold"
        },
        "source": {
            "type": "uritemplate",
            "description": "source of the event",
            "value": "{vehicleid}/{componentid}"
        },
        "subject": {
            "type": "string",
            "description": "identifier of the temperature alarm"
        },
        "datacontenttype": {
            "value": "application/vnd.apache.avro+plainjson"
        },
        "time": { "required": true }
    },
    "schemaformat": "Avro/1.11.1",
    "schemauri": "#/schemagroups/Contoso.MotorVehicle/schemas/Contoso.MotorVehicle.OilTemperatureThresholdExceededEventData"
}

We believe that is the right choice for 90% of the cases, but it's a recommendation and not a requirement since there are cases where the two identifiers will be different. The discussion below will be interesting to CloudEvent users even if you don't care about xRegistry at the moment.

What does the CloudEvents type attribute mean?

The type attribute of a CloudEvent is an identifier of the kind of occurrence, the thing that happened that the event reports about. A database record was added, a river level gauge reading was made, the oil temperature of a motor exceeded a threshold value. The type identifies those semantics.

The type is not coupled to a specific payload schema or schema version or schema format or data encoding. That is why the CloudEvent has the dataschema and datacontenttype attributes to individually qualify these aspects. 

The type is, however, supposed to be coupled to stable semantics. That is to say that when you define an event type together with an initial payload, it's allowed for the payload to be refined over time by adding more information (since you can convey this via dataschema) and it's also allowed to encode the information differently (since you can convey this via datacontenttype).

It's not allowed for the semantic essence of event to change under a given type. The event still needs to report about the same kind of occurrence. Any breaking change, omitting formerly required information, is a semantic change and ought not to be permitted within a type. If the semantics change, you must introduce a new type and you may indeed have to report the same occurrence once per any existing and newly introduced type if you make such a choice. 

We did not introduce "event type versions" as an easy switch in CloudEvents. We specifically wanted to put up a high bar for breaking changes. Distributed systems are hard enough while being disciplined about sticking to promises made. They turn into chaos if breaking promises is easy. We effectively mandate that a breaking change is signaled by a whole new type.

Now, I work with quite a few developers absolutely obsessed with precision and I know that "semantic identifier of an occurrence" freaks them out as a concept because it's kinda wobbly. But the lack of specificity around the expression of the event semantics in terms of encodings and schemas is intentional because only that enables eventual evolution. Today, JSON encoding is overwhelmingly the favorite way to move event data, but tomorrow the fashion may change. The encoding choice changing, and with it the schema format required to drive that encoding, does not invalidate the semantic of the event. The the oil temperature of the motor still exceeds its threshold value, even if the expression of it changes.

xRegistry message definitions

xRegistry aims to be a catalog for messaging and eventing, organizing endpoints for discovery, organizing data schemas for sharing validation and serialization metadata, and organizing message metadata to declare "contracts" for communication channels.

You can think of a message definition like the one shown above as a set of constraints (or a template) for an event. The definition above defines a specific expression of a CloudEvent of type Contoso.MotorVehicle.OilTemperatureThresholdExceeded. The definition references an Apache Avro schema (not shown) and mandates for the encoding to be "vnd.apache.avro+plainjson" via a constraint of the datacontenttype attribute.

That is, however, just one way how one might to express this event in a message. You could have an alternate representation using Protobuf3.   

"Contoso.MotorVehicle.OilTemperatureThresholdExceeded.Proto3": {
    "description": "Alarm event for when the oil temperature exceeds a threshold",
    "envelope": "CloudEvents/1.0",
    "envelopemetadata": {
        "id": { "required": true },
        "type": {
            "value": "Contoso.MotorVehicle.OilTemperatureThresholdExceeded",
            "description": "Event raised when oil temperature exceeds a set threshold"
        },
        "source": {
            "type": "uritemplate",
            "description": "source of the event",
            "value": "{vehicleid}/{componentid}"
        },
        "subject": {
            "type": "string",
            "description": "identifier of the temperature alarm"
        },
        "datacontenttype": {
            "value": "application/vnd.google.protobuf.v3"
        },
        "time": { "required": true }
    },
    "schemaformat": "Protobuf/3",
    "schemauri": "#/schemagroups/Contoso.MotorVehicle/schemas/Contoso.MotorVehicle.OilTemperatureThresholdExceededEventData.Proto3"
}

This definition is semantically identical as it shared the event type. Once you've deserialized the event into memory, the difference between the versions that flowed as Protobuf and the one that flowed as Avro-defined JSON should vanish completely if the schemas are, as they should, expressing semantically identical data structures.

In a constellation where we want to flow events in different encodings, we will therefore have multiple definitions that have distinct message definition identifiers (messageid), but which refer to the same type as they express the same semantics. A basic test for whether the structurally expressed semantics match across the variants of the event is whether the schemas for the respective encodings can be mapped to the same in-memory data structure easily.

The specificity of a message definition in that it ties together several constraints also means that it's not versionable per-se; message types have variants, not versions. Consequently, xRegistry does not version message definitions, different from how it handles data schemas.

If you change the composition of the constraints, you create a new definition. That is the case because if you ever produced a message based on the given set of constraints, another party using the catalog needs to be able to have the definition available to match against, side-by-side with all other variants. If a message arrives with type Contoso.MotorVehicle.OilTemperatureThresholdExceeded and datacontenttype application/vnd.google.protobuf.v3, having both of the definitions above available concurrently allows matching and subsequently processing according to the metadata/constraints of the second (*.Proto3) one.  

The application platform layers that deal with dispatching and deserialization or channel selection and serialization will distinguish events by messageid and, in the dispatcher case, pattern-match incoming events against the concurrently available message definitions to find one specific match. The application layers above will distinguish events by their CloudEvents type as all alternate definitions and their alternate wire representations ought to yield compatible events to the application after processing. 


I wrote this down primarily to capture today's WG discussion, but I hope you also find this useful as a CloudEvents practitioner.

[Note: A few links in this document don't deep-link to where they should; that's due to a bug in my fork of the blog engine that I'll fix shortly]

 

Share on Twitter, Reddit, Facebook or LinkedIn