Manual:Domain events/Report FY24-25
Over the course of the fiscal year 2024-25, the MediaWiki Interfaces team has introduced a system of Domain Events into MediaWiki. Domain Events provide a new way for extensions and core components to be notified about changes in other parts of the system, offering a more expressive and sustainable alternative to hooks that were previously used for this purpose. The need for this new capability was identified by a survey of hook usage we conducted in the first quarter.
The work on the domain events system was conducted in close collaboration with Data Platform Engineering with substantial support from the Growth team and the Language team. These collaborations have proven immensely valuable by allowing us to iterate on the design early on and fostered knowledge sharing across the teams.
Our efforts have resulted in a simple yet powerful system that has already proven effective in avoiding coupling between core components and reducing the need for boiler plate code in extensions. The domain events framework is part of the MediaWiki 1.44 release. A first set of events is scheduled to become part of MediaWiki’s stable interface in 1.45.
Motivation

The goal of introducing domain events into MediaWiki is to reduce coupling in multiple ways: firstly, introducing an event dispatcher allows us to apply the observer pattern in MediaWiki, which reduces coupling between core components. Secondly, events can replace certain kinds of hooks, providing a more robust and sustainable interface. Thirdly, modeling state change of MediaWiki entities as events makes it much easier to broadcast these changes over a message bus such as Kafka, allowing us to integrate MediaWiki into an event-based architecture more naturally and tightly.
Following the idea of domain events as defined in domain driven design, MediaWiki DomainEvents represent changes to the observable state of “entities” such as pages or users. This matches the behavior that had evolved organically for the 13 hooks that we classified as “events” in the survey we conducted on hook usage in Q1. Events can replace these hooks, providing a more expressive and robust way for extensions to get notified about changes they are interested in.
Several problems have become evident with the existing system of using hooks to inform extensions about changes:
- Hook signatures are rigid, once defined they cannot be changed. Adding or remodeling information passed to hook handlers is not possible without introducing a new hook and deprecating the old one.
- Hook signatures are often informed by the context in which the hook is triggered, rather than being designed to represent a well defined kind of change to a conceptual entity. That makes it hard to build an external representation of the event, to be published to consumers outside MediaWiki.
- Hooks are typically, but not always, invoked synchronously during the main transaction, while the client waits for a response. This means that hook handlers may disrupt the calling code in the case of failures, and can interfere with the ongoing database transaction. It also leads to a further issue:
- Hook handlers have to be careful not to add latency that would cause undue retention of database locks which could cause performance problems and even outages. Because of this, hook handlers that need to write to the database typically use a DeferredUpdate for this purpose, which gets executed later, after the response has been sent to the client. This however leads to another complication:
- If the main transaction is rolled back, the deferred update still gets executed. This often leads to inconsistencies, since the purpose of the hook handler is typically to record extension-specific information based on a state change in MediaWiki core.
- Hooks often expose complex mutable objects that may change state depending on when during the request life cycle they are accessed. This has led to subtle bugs and data corruption many times in the past, especially in conjunction with DeferredUpdates.
The domain events system was designed to address these problems as follows:
- Events are represented by immutable objects that are designed to model specific changes. The data in the events can be re-modeled with minimal disruption for listeners.
- Events get delivered to listeners via a DeferredUpdate after the main transaction was successful and the response has been sent to the client. This clarifies the semantics of extension callbacks invoked as a result of a change, particularly with respect to transactional context.
- The EventIngress base class can be used to provide convenience methods to listeners, avoiding boilerplate code.
This system provides a more convenient and robust interface to extensions, but it can also be used to reduce coupling between components of MediaWiki core: it removes the need for undesirable dependencies when one component has to be informed about changes performed by another. Until now, code that changed state needed to know which other components it had to inform about that change, and call them directly. Domain events can be used to avoid such cross-dependencies by applying the observer pattern: if an event is emitted after a state change, any code that is interested in the change can register a listener for it without the code that affects the change needing to know about it. This improves encapsulation in the spirit of separation of concerns: a component that changes state no longer needs to know about the concerns of components that are affected by that state change.
Architecture

The architecture for dispatching Domain Events to in-process listeners follows established patterns for event processing, with the distinction that, in contrast to event dispatching in most web frameworks, event delivery is deferred until after the main database transaction has been completed and the response has been sent to the client.
The design of the infrastructure for modeling and dispatching domain events defines the following key concepts:
Event Objects are value objects that represent a change in the state of an entity. Each event object has an event type that listeners can subscribe to. Event types form a hierarchy, so listeners that subscribe to a more general event type will also receive all more specific events.
Event Listeners are methods that are invoked by the Dispatch Engine after an event of a certain type occurred. That is, the listener is invoked if it was previously registered with the Event Source and then an Event Object of the relevant type was emitted through the Event Dispatcher. Listeners are typically implemented as methods on an Ingress Object.
The Event Dispatcher is a service object that can be used to emit event objects which will later be delivered to listeners.
The Event Source is a service object that offers methods for registering listeners for any type of event. However, registering listeners directly with the event source should be the exception. Typically, this is left to an Ingress Object.
Ingress Objects implement a group of related Listener methods. They act as an adapter between the data the producer included in the incoming events and the logic inside the consumer component/extensions. Declaring Ingress Objects in extension.json is the preferred way for extensions to register listeners.
The Dispatch Engine is the concrete implementation that routes events to registered listeners - it implements both DomainEventDispatcher and DomainEventSource. Understanding that Event Dispatcher and Event Sources are different perspectives on the same object is important for understanding the flow of information. However, application logic will always interact either with one interface or the other, never with the engine as such.
When application logic calls the dispatch() method on the Event Dispatcher interface, the Dispatch Engine does the following:
- Create a dispatcher class that builds on top of DeferredUpdate for deferred, transaction-bound dispatching
- Allow listeners to be registered with the dispatcher. Use the subscriber pattern to allow listeners to be bundled to improve internal cohesion of consumer side code, and make listener registration simpler for extensions.
- Allow lazy instantiation for subscribers: when registering a subscriber as an object spec, it will be invoked only when any of the events it registers for are triggered.
- When an event is triggered, create any pending ingress objects for the event’s type (or parent types) , and allow them to register their listeners. Then schedule a DeferredUpdate for invoking each listener for that event.
- The DeferredUpdate will be bound to the current transaction of the database connection passed along with the event, so it will be canceled automatically if the transaction fails.
- After the transaction is complete, and after the response has been sent to the client, the DeferredUpdates are executed, invoking each registered handler in a separate transaction context.
Modeling
The idea of domain events in MediaWiki is inspired by domain driven design where the term is used to describe something that happened in the past that users care about, typically something that changes the visible state of the system. Martin Fowler writes that "the essence of a Domain Event is that you use it to capture things that can trigger a change to the state of the application you are developing" [1].
More specifically, domain events in MediaWiki represent a change in the observable state of a conceptual entity like a page or a user. Such high level entities are often aggregates that consist of smaller (sub) entities: e.g. a page may be said to consist of page record and the page history, which in turn consists of revisions. Code that changes the state of entities is responsible for maintaining consistency among the members of the aggregate, e.g. when a page is edited the page history and page record must be updated atomically. It is this code that will emit a domain event when the change is complete.
To gain some real life experience with modeling events and using them in extensions, we introduced events that represent changes to wiki pages. This work was supported by an adjacent hypothesis that explored the definition of domains based on user workflows. We created the following event model:
- PageEvent: Base type for all events representing changes to the page aggregate
PageHistoryEvent: The change affects historical revisions of the page, potentially including the current revision. This event type is currently not modeled explicitly but could be added in the future to act as a shared base type for events that affect historical revisions.- PageHistoryVisibilityChangedEvent: some aspects of some revisions was hidden or suppressed
- PageProtectionChangedEvent: The page was protected (or unprotected). This doesn’t affect the current state of the page.
- PageRecordChangedEvent: Changes to the current state of the page. This kind of change requires the page to be rerendered.
PageIdentityChangedEvent: The change affects the identity (title or id) of the page. This event type is currently not modeled explicitly but could be added in the future to act as a shared base type for events that affect the page identity.- PageCreatedEvent: The page was created.
- PageDeletedEvent: The page was deleted.
- PageMovedEvent: The page was moved.
- PageLatestRevisionChangedEvent: The latest revision of the page changed - typically because of an edit, but may be caused by other actions as well, such as rollbacks or page moves.
While modeling these events we came across several challenges that required us to adjust our modeling and clarify the boundaries of entities and the semantics of the events and their properties. One of the first challenges we encountered were null edits: when a user opens the edit page and then saves the page again without making any changes, this triggers re-processing of the page content as if the page has changed, but does not create a new entry in the page history. So the question was - should null edits trigger an event just like an edit would? After a lot of discussion, we decided that yes, null edits should trigger the same kind of event as real edits, because they are basically reconciliation requests: they are used to manually repair inconsistencies that were introduced by bugs or outages. Because of this, downstream consumers should generally behave as if the page had been edited.
Another complication arose around imports and undeletions: imports and undeletions typically create pages, but not always - it is possible to import (and undelete) historical revisions and attach them to an existing page. In that case, they may or may not update the latest revision of the page. So imports and undeletions may (or may not) trigger a PageCreatedEvent, and the ones that do not may (or may not) trigger a PageLatestRevisionChangedEvent, depending on whether they change the latest revision.
We encountered several more such complications, e.g. around dummy revisions, which get recorded to the history but do not change page content; and around page moves, which may involve the creation and deletion of other pages.
The model we arrived at is the result of many hours of exploring the implications of different modeling options and balancing semantic accuracy against ease of use.
Impact
Since we immediately started to use the domain events system while developing it, we were able to observe its impact right away. By the end of the fiscal year, nine extensions adopted domain events. This enabled us to optimize for common usage patterns, avoid unnecessary complexity and quickly iterate to prevent undesirable side effects.
The most obvious impact that the adoption of domain events has on extensions is the removal of boilerplate code for scheduling DeferredUpdates in hook handlers. Beyond reducing code duplication this has the additional benefit of addressing a common issue with that boilerplate code, namely that it often fails to account for the case that the main transaction is rolled back. Handling for rollbacks is built into the domain event system.
Another effect that we observed was reduced reliance on old “monster classes” like Title and WikiPage. Several times, the adoption of domain events has triggered further refactoring of extension code to remove usages of these classes in favor of the value objects provided by the events. This proved particularly beneficial in the case of the EventBus extension where it solved a whole class of recurring subtle bugs related to the stateful nature of WikiPage.
In addition to improving extensions, we also experimented with using domain events to reduce coupling between core components. This allowed us to remove several cross-dependencies from code that performs page updates: previously, this code needed to know about things like updating the search index, incrementing user edit counts, purging caches for system messages and resource loader modules, etc. Now, the respective components register ingress objects to perform these updates. This allowed us to remove numerous dependencies from page updater code.
These observations confirm that the domain events system is successful in achieving the goal we designed it for: reducing coupling, avoiding boiler plate code, and making extensions more robust by providing an expressive interface optimized for common use cases.
Outlook
Moving forward, we hope that more teams will adopt the initial set of events we defined and also start to define their own. For now, we have only modeled basic operations on pages as domain events. There is a lot of potential in modeling events for more entities in MediaWiki, such as log entries, user accounts, and rendered page output. The latter in particular would allow us to replace the brittle mechanisms currently used to feed downstream consumers of page HTML, such as Enterprise and the Wikipedia App.
In the future, domain events will hopefully also help to integrate MediaWiki into an event-based architecture that enables data-backed features: generalizing the mechanism for broadcasting events via Kafka that is currently implemented in EventBus would make it much easier to inform other processes about state changes in MediaWiki. But there is even more potential in providing a simple mechanism for MediaWiki to receive events from other services. This would remove the need for creating an ad-hoc push API for every new kind of thing that a service may want to inform a MediaWiki about.
In conclusion, further adopting domain events will support modularization, improve robustness and reduce boilerplate code in extensions. It will also help make MediaWiki a part of a larger event-based architecture and help replace brittle legacy data flows with more resilient data pipelines.