This is a simple demonstration of a domain event factory in Python. I assume you are familiar with the Factory Method Pattern. I also use the pydantic package for attribute validation. When implemented, we can use the factory to create immutable domain events with a homogenous data structure across instances of the same type. The metadata is generated by the underlying BaseEvent. In this approach we always produces complete events.

Start with all our imports:

from datetime import datetime
from typing import Union, List
from uuid import UUID, uuid4

from pydantic import BaseModel
from pydantic.fields import Field, ModelField

Base Event class

the BaseEvent includes metadata ( id and created_at ) for all events. The BaseModel.Config sets the object to be immutable and no extra arguments are allowed:

class EventMetaData(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)
    class Config:
        allow_mutation = False
        extra = "forbid"

class BaseEvent(BaseModel):
    """Generic domain event."""

    class Config:
        allow_mutation = False
        extra = "forbid"

    id: UUID = Field(default_factory=uuid4)
    metadata: EventMetaData = Field(default_factory=EventMetaData)

we introduce a method that returns a new event type from the BaseEvent with a name and an additional datastructure:

def create_event_type(
    event_type: str,
    data_structure: dict,
    doc_string: Union[str, None] = None,
):
    """Return new domain event type."""
    _cls = type(event_type, (BaseEvent,), {})
    _cls.__annotations__.update(data_structure)
    if doc_string:
        _cls.__doc__ = doc_string
    _fields: Dict[str, ModelField] = {}
    for fname, f_def in data_structure.items():
        f_val = ...
        if isinstance(f_def, tuple):
            f_def, f_val = f_def
        _fields[fname] = ModelField.infer(
            name=fname,
            value=f_val,
            annotation=f_def,
            class_validators=None,
            config=_cls.__config__,
        )
    _cls.__fields__.update(_fields)
    return _cls

A simle demonstration of the create_event function:

>>> CommentWritten = create_event_type("CommentWritten", {"author": str})
>>> CommentWritten(author="joost").json()
'{"id": "85990e7e-9705-4450-849e-70a764593e81", "metadata": {"created_at": "2021-01-25T11:45:19.998831"}, "author": "joost"}'

The EventTypeFactory

The factory uses the create_event_type function to register Domain Event Type classes:

class EventTypeFactory:
    def __init__(self):
        self._event_types = {}
        self._id_generator = lambda: None

    def register_event_type(self, event_type: str, data_structure: dict):
        self._event_types[event_type] = create_event_type(
            event_type, data_structure, self._id_generator
        )

    def get_event_type(self, event_type):
        event_type = self._event_types.get(event_type)
        if not event_type:
            raise ValueError(event_type)
        return event_type

Now we can register EventTypes with a data structure:

>>> factory = EventTypeFactory()
>>> factory.register_event_type(
    "BoughtWineEvent",
    {"year": int, "wine": str},
)
>>> factory.register_event_type(
    "AnotherEvent",
    {"ready": bool},
)

and use the factory to get event type classes:

>>> BoughtWineEvent = factory.get_event_type("BoughtWineEvent")
>>> print("type of event:", type(BoughtWineEvent))
type of event: <class 'pydantic.main.ModelMetaclass'>
>>> event = BoughtWineEvent(
    year=2008,
    wine="Pinot Noir",
)
>>> event.json()
'{"id": "f880be5c-d352-4149-abfa-e5f02a61eb92", "metadata": {"created_at": "2021-01-25T11:46:09.222415"}, "year": 2008, "wine": "Pinot Noir"}'

I’m still considering nesting the metadata and data in designated keys but otherwise the factory works perfectly :)