122. Event-Driven Order Management
Status: Accepted Date: 2025-07-06
Context
The Kairos module manages the lifecycle of simulated orders. When an order's status changes (e.g., from ACTIVE to FILLED), other parts of the system need to be notified. For example, the Morpheus module needs to know so it can update the parent shadow position, and the Atlas module needs to know so it can include the filled order in its performance reports.
A direct-calling approach, where Kairos would have to explicitly call morpheusService.handleOrderFilled() and atlasService.handleOrderFilled(), would create tight, brittle coupling. Kairos would need to know about every single module that might be interested in order events, which is not scalable.
Decision
The Kairos module will use an event-driven design for broadcasting order state changes. It will use the EventEmitter2 library for in-process eventing.
When a significant action occurs in the Kairos module, it will emit a corresponding event. Examples include:
order.createdorder.cancelledorder.filled
The event payload will contain the full order object, providing all necessary context.
Other modules, such as Morpheus or Atlas, can then subscribe to these specific events. They can register their own handler functions to be executed when an event they care about is emitted. This creates a clean, decoupled architecture. Kairos does not need to know who is listening; its only responsibility is to announce that something has happened.
Consequences
Positive:
- Decoupling:
Kairosis completely decoupled from the modules that consume its events. We can add new listeners (e.g., a new real-time alerting module) without making any changes toKairos. - Flexibility and Extensibility: This design is incredibly flexible. Any module can easily tap into the stream of order events to add new functionality.
- Single Responsibility:
Kairoscan focus on its single responsibility: managing the order lifecycle correctly. It doesn't need to be concerned with the downstream business logic of other modules. - Real-Time Updates: Using a synchronous event emitter like
EventEmitter2ensures that interested modules are notified of state changes in real-time, within the same process.
Negative:
- In-Process Only:
EventEmitter2is an in-process event bus. This solution is not suitable for communicating between different services or processes. All listeners must be running in the same NestJS application instance. - Risk of Complex Event Chains: It's possible to create complex, hard-to-debug chains of events where one event handler triggers another event, which triggers another.
- No Durability: If the server crashes after an order is filled but before the event is fully handled by all subscribers, the event is lost.
Mitigation:
- Appropriate Scope: This eventing model is used within our modular monolith, where all modules run in the same process. For inter-service communication, we use a persistent message queue (BullMQ), which is a different pattern for a different problem.
- Developer Discipline: We must be disciplined about keeping event handlers simple and focused on a single task to avoid creating complex, cascading updates.
- Complementary State Reconciliation: While events are used for real-time updates, the system does not rely only on them for consistency. The state of the order is persisted in the database, and we can have reconciliation jobs that periodically verify the consistency between parent positions and their orders, ensuring that no state changes are permanently lost.