# How EventBus Pub/Sub Communication Works Between Fincept Modules

> Discover how Fincept modules use EventBus pub/sub for decoupled communication with std::function handlers and thread-safe Qt queued connections. Learn more now.

- Repository: [Fincept Corporation/FinceptTerminal](https://github.com/Fincept-Corporation/FinceptTerminal)
- Tags: internals
- Published: 2026-04-20

---

**Fincept modules communicate through a singleton EventBus that enables decoupled publish/subscribe patterns using std::function handlers and thread-safe Qt queued connections.**

The FinceptTerminal codebase relies on a lightweight **EventBus** implementation to enable loose coupling between UI screens, trading engines, and data tools. This **publish/subscribe (pub/sub)** architecture allows any module to broadcast events without hard-wired dependencies on listeners. Understanding how this **EventBus pub/sub communication** works is essential for extending the platform or debugging event flows across the `fincept-qt` source tree.

## Core Architecture of the EventBus

The **EventBus** is implemented as a **singleton** QObject defined in [`fincept-qt/src/core/events/EventBus.h`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/fincept-qt/src/core/events/EventBus.h) and implemented in [`fincept-qt/src/core/events/EventBus.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/fincept-qt/src/core/events/EventBus.cpp). It maintains an internal map of event names to handler lists, providing three primary operations:

- **`subscribe(event, handler)`**: Registers a `std::function<void(const QVariantMap&)>` callback for a specific event string and returns a **HandlerId** (integer) for later removal.
- **`publish(event, data)`**: Dispatches a `QVariantMap` payload to all registered handlers for that event name.
- **`unsubscribe(id)`**: Removes a handler using the **HandlerId** returned during subscription.

The **singleton pattern** (`EventBus::instance()`) guarantees that every module references the same bus instance throughout the application lifecycle. Because the class inherits from QObject, it leverages Qt's meta-object system to ensure handlers execute on the correct thread.

## Real-World Pub/Sub Patterns in Fincept

The Fincept codebase demonstrates **EventBus pub/sub communication** across multiple modules without compile-time coupling:

**Navigation Events**
- **Publisher**: [`NavigationTools.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/NavigationTools.cpp) (line 115) emits `"nav.switch_screen"` with a payload containing the target screen name.
- **Subscriber**: [`MainWindow.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/MainWindow.cpp) (line 274) listens for `"nav.switch_screen"` to execute actual screen transitions.
- **Additional Publisher**: [`SettingsScreen.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/SettingsScreen.cpp) (line 1954) publishes the same event when users click navigation buttons.

**Research Workflows**
- **Publisher**: [`CommandBar.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/CommandBar.cpp) (line 1267) broadcasts `"equity_research.load_symbol"` when users enter ticker symbols.
- **Subscriber**: [`EquityResearchScreen.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/EquityResearchScreen.cpp) (line 41) subscribes to load financial data for the requested symbol without directly referencing the command bar.

**Trading System Events**
- **Publisher**: [`PaperTrading.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/PaperTrading.cpp) (line 319) emits `"paper_trading.order_filled"` after simulated trades execute.
- **Publisher**: [`OrderMatcher.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/OrderMatcher.cpp) (line 282) broadcasts `"paper_trading.position_update"` when positions change.
- **Publisher**: [`WatchlistTools.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/WatchlistTools.cpp) (line 70) publishes `"watchlist.created"`, `"watchlist.deleted"`, and `"watchlist.updated"` for CRUD operations.

This decoupling allows the trading engine to emit events without knowing which UI screens (if any) are listening, and allows screens to react to data changes without importing trading module headers.

## Thread Safety and Dispatch Mechanism

The **EventBus** guarantees thread safety through Qt's **queued connection** mechanism. When `publish()` is called from a worker thread, the implementation marshals the call to the bus's own thread (the UI thread) before invoking handlers.

As implemented in [`fincept-qt/src/core/events/EventBus.cpp`](https://github.com/Fincept-Corporation/FinceptTerminal/blob/main/fincept-qt/src/core/events/EventBus.cpp), the dispatch uses `QMetaObject::invokeMethod` with `Qt::QueuedConnection`. This ensures that UI code never executes off the main thread, preventing race conditions in widget updates while allowing background tasks to trigger UI changes safely.

## Implementing Pub/Sub in Practice

To participate in **EventBus pub/sub communication**, modules follow a three-step pattern:

### Subscribing to Events

```cpp
// In a screen constructor (e.g., EquityResearchScreen.cpp)
handlerId = EventBus::instance().subscribe(
    "equity_research.load_symbol",
    [this](const QVariantMap& data) {
        QString symbol = data.value("symbol").toString();
        loadFinancialData(symbol);
    });

```

Store the returned **HandlerId** as a member variable for cleanup.

### Publishing Events

```cpp
// When a user action triggers navigation (e.g., CommandBar.cpp)
QVariantMap payload;
payload.insert("symbol", userInput);
EventBus::instance().publish("equity_research.load_symbol", payload);

```

### Unsubscribing on Destruction

```cpp
// In the class destructor to prevent dangling callbacks
EventBus::instance().unsubscribe(handlerId);

```

This pattern appears throughout `fincept-qt/src/screens/` and `fincept-qt/src/mcp/tools/`, ensuring widgets clean up their subscriptions when destroyed.

## Summary

- **EventBus** is a **singleton** QObject providing centralized **pub/sub** functionality via `subscribe()`, `publish()`, and `unsubscribe()`.
- Handlers are `std::function<void(const QVariantMap&)>` callbacks identified by **HandlerId** integers.
- Event names are plain strings (e.g., `"nav.switch_screen"`, `"paper_trading.order_filled"`) with no centralized registry required.
- **Thread safety** is enforced through Qt queued connections, ensuring handlers always run on the UI thread.
- Modules in `fincept-qt/src/trading/` publish business events while modules in `fincept-qt/src/screens/` subscribe, maintaining zero compile-time dependencies between layers.

## Frequently Asked Questions

### How does the EventBus ensure thread safety when publishing from background threads?

The **EventBus** uses `QMetaObject::invokeMethod` with `Qt::QueuedConnection` inside `EventBus::publish()`. If a worker thread calls publish, Qt automatically queues the handler invocation to the bus's thread (the main UI thread), ensuring UI updates never occur from background contexts.

### What data type does the EventBus use for event payloads?

All payloads are **`QVariantMap`** (Qt's variant dictionary). Publishers insert key-value pairs using `payload.insert("key", value)`, and subscribers extract data using `data.value("key").toType()`. This provides type-safe, flexible data transfer without custom event classes.

### Can a module both publish and subscribe to the same event?

Yes. Any module can call both `subscribe()` and `publish()` for identical event strings. The bus maintains a list of all registered handlers and invokes them sequentially when `publish()` is called, regardless of whether the publisher is also a subscriber.

### How do I remove an event subscription when a widget is destroyed?

Store the **HandlerId** returned by `subscribe()` as a member variable (typically `int eventHandlerId_`). In the widget's destructor, call `EventBus::instance().unsubscribe(eventHandlerId_)`. This prevents callbacks from firing after the widget's memory is released.