# The Mental Model: A Layered Modular Monolith

> The simplest useful picture of Fincept Terminal — six stacked layers (Presentation → Application → Data Plane → Adapters → Infrastructure → Platform), 13 bounded contexts that publish to each other only via DataHub topics or typed events, and one Qt6 binary per OS with embedded Python for analytics. Reading this page should let you guess which directory a new feature belongs in and why cross-context direct calls are forbidden.

- Repository: Fincept-Corporation/FinceptTerminal
- GitHub: https://github.com/Fincept-Corporation/FinceptTerminal
- Human wiki: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f
- Complete Markdown: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f/llms-full.txt

## Source Files

- `README.md`
- `docs/ARCHITECTURE.md`
- `fincept-qt/CMakeLists.txt`
- `fincept-qt/src/app`
- `fincept-qt/src/datahub`
- `fincept-qt/src/services`
- `fincept-qt/src/screens`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
- [fincept-qt/CMakeLists.txt](fincept-qt/CMakeLists.txt)
- [fincept-qt/src/datahub/DataHub.h](fincept-qt/src/datahub/DataHub.h)
- [fincept-qt/src/datahub/Producer.h](fincept-qt/src/datahub/Producer.h)
- [fincept-qt/src/app/DockScreenRouter.h](fincept-qt/src/app/DockScreenRouter.h)
- [fincept-qt/src/services](fincept-qt/src/services)
- [fincept-qt/src/screens](fincept-qt/src/screens)
- [fincept-qt/src/trading/BrokerInterface.h](fincept-qt/src/trading/BrokerInterface.h)
- [README.md](README.md)
</details>

# The Mental Model: A Layered Modular Monolith

Fincept Terminal is *one* native Qt6 desktop binary per OS, internally divided into six stacked layers and thirteen bounded contexts. The whole point of this page is that once you can name the six layers and the thirteen contexts, you can predict — without grepping — which directory a new feature belongs in, which layer it's allowed to call into, and which it must talk to only via DataHub topics or typed events. If you walk away with anything, walk away with the rule: **dependency direction is downward only, and cross-context conversations go through the data plane, never through a direct `#include`.**

This is deliberately a *modular monolith*, not a service mesh. The runtime is a single process; the modularity is enforced by topology and by the DataHub publish/subscribe seam — not by network boundaries.

Sources: [docs/ARCHITECTURE.md:11-78](docs/ARCHITECTURE.md), [README.md:1-40](README.md)

## The Six Layers

The codebase is sliced into six horizontal layers. Each layer may depend only on the layer beneath it; reversing the arrow is a bug, not a style issue.

```text
┌──────────────────────────────────────────────────────────────┐
│  PRESENTATION   54 screens · 13 dashboard widgets · ADS dock │  src/screens, src/ui
├──────────────────────────────────────────────────────────────┤
│  APPLICATION    ~50 services across 13 bounded contexts      │  src/services
├──────────────────────────────────────────────────────────────┤
│  DATA PLANE     DataHub (pub/sub) + CacheManager (TTL/SQLite)│  src/datahub, src/storage/cache
├──────────────────────────────────────────────────────────────┤
│  ADAPTERS       Broker · MCP · PythonRunner · HTTP · WS      │  src/trading, src/mcp, src/python, src/network
├──────────────────────────────────────────────────────────────┤
│  INFRASTRUCTURE Logger · Config · EventBus · Auth · Storage  │  src/core, src/auth, src/storage
├──────────────────────────────────────────────────────────────┤
│  PLATFORM       Qt6 (Widgets/Charts/Network/Sql/WebSockets)  │  vendor
└──────────────────────────────────────────────────────────────┘
```

The four invariants that hold the model together, copied verbatim from the architecture doc:

1. **Presentation → Application → Data Plane → Adapters → Infrastructure → Platform. Never reverse.**
2. **Adapters are leaves.** Two services may share an adapter; an adapter may not call a service.
3. **Cross-context calls go through DataHub topics or typed events, never direct includes.**
4. **Infrastructure has no business knowledge.** It does not know what a "watchlist" is.

If you violate (1), CMake will eventually catch you — the target shape splits the build into per-module library targets (`fincept_core`, `fincept_ui`, `fincept_network`, `fincept_storage`, `fincept_datahub`, …) so circular dependencies become *physically impossible* at link time. Today the build is still a single ~3,354-line `CMakeLists.txt`, so the discipline is currently social rather than mechanical.

Sources: [docs/ARCHITECTURE.md:31-79](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:304-325](docs/ARCHITECTURE.md), [fincept-qt/CMakeLists.txt:1-80](fincept-qt/CMakeLists.txt)

## The Thirteen Bounded Contexts

The Application layer is not a flat pile of services. It is divided into thirteen bounded contexts, each of which owns its own screens, services, types, and DataHub topic prefixes. A feature lives entirely inside one context, or it is explicitly cross-cutting and tracked as such.

| # | Context | Topic prefix | Lives under |
|---|---------|--------------|-------------|
| 1 | Markets | `market:*`, `watchlist:*` | `services/markets`, `services/equity`, `services/sectors` |
| 2 | News | `news:*` | `services/news` |
| 3 | Economics | `econ:*` | `services/economics`, `services/dbnomics` |
| 4 | Geopolitics | `geopolitics:*` | `services/geopolitics`, `services/maritime` |
| 5 | Trading | `broker:<id>:*`, `paper:*` | `src/trading`, `services/algo_trading` |
| 6 | Portfolio | `portfolio:*` | `services/portfolio` |
| 7 | Crypto | `ws:<exchange>:*`, `wallet:*` | `services/crypto`, `services/wallet` |
| 8 | Derivatives | `derivatives:*` | `services/options` |
| 9 | Predictions | `prediction:*` | `services/polymarket`, `services/prediction` |
| 10 | Agents | `agent:<kind>:run:<id>` | `services/agents`, `services/alpha_arena` |
| 11 | AI Chat | event-driven | `services/llm` |
| 12 | Workflow | `workflow:*` | `services/workflow` |
| 13 | Identity | event-driven | `src/auth`, `core/identity`, `services/billing` |

The directory listing under `fincept-qt/src/services/` (40+ subfolders such as `markets/`, `news/`, `economics/`, `geopolitics/`, `polymarket/`, `wallet/`, `workflow/`, `agents/`) reflects this carving, with some contexts owning more than one folder (Markets owns `markets/`, `equity/`, `sectors/`, `asia_markets/`).

Two contexts deliberately do **not** use DataHub: AI Chat and Identity are event-driven (typed events on `EventBus`/typed signals) because their interactions are one-shot, not subscription-shaped.

Sources: [docs/ARCHITECTURE.md:101-122](docs/ARCHITECTURE.md)

## How a Feature Flows Across the Stack

The mental model only earns its keep if you can use it to *predict layout*. The diagram below shows the runtime topology for a representative read-path (a screen showing AAPL quotes) and the architectural rule each arrow obeys.

```mermaid
flowchart TB
    subgraph Presentation["Presentation (src/screens, src/ui)"]
        MarketsScreen["MarketsScreen<br/><i>QWidget · IStatefulScreen</i>"]
        Dashboard["DashboardWidgets (13)"]
        Router["DockScreenRouter<br/>(lazy factory of 54 screens)"]
    end

    subgraph Application["Application (src/services)"]
        direction LR
        Markets["MarketDataService<br/><i>Producer · market:quote:*</i>"]
        News["NewsService<br/><i>Producer · news:*</i>"]
        Trading["UnifiedTrading"]
        Wallet["WalletService<br/><i>UI-coordinator</i>"]
    end

    subgraph DataPlane["Data Plane (src/datahub, src/storage/cache)"]
        Hub["DataHub<br/>topic = domain:subdomain:id"]
        Cache["CacheManager (SQLite TTL)"]
    end

    subgraph Adapters["Adapters (leaves — never call services)"]
        HTTP["HttpClient"]
        WS["WebSocketClient"]
        Py["PythonRunner (QProcess)"]
        Broker["BrokerInterface (16 impls)"]
        MCP["McpService (40+ tools)"]
    end

    subgraph Infra["Infrastructure (src/core, src/auth, src/storage)"]
        Logger["Logger"]
        Auth["AuthManager · SessionGuard"]
        DB["Database · SecureStorage<br/>(AES-256-GCM)"]
        Bus["EventBus (typed events)"]
    end

    MarketsScreen -- "subscribe('market:quote:AAPL')" --> Hub
    Dashboard -- subscribe --> Hub
    Router -.lazy create.-> MarketsScreen

    Hub -- "cache hit? deliver" --> Cache
    Hub -- "stale? refresh()" --> Markets
    Markets -- async --> HTTP
    Markets -- async --> Py
    Markets -- "publish('market:quote:AAPL', …)" --> Hub

    Trading --> Broker
    Wallet -- dialog parent --> MarketsScreen
    MCP --> Trading

    Markets --> Logger
    Broker --> Auth
    Auth --> DB
    HTTP --> Bus
```

Two things in that diagram are worth dwelling on:

- **`MarketsScreen` never touches `HttpClient` directly.** The screen subscribes to a topic; the service decides whether to serve from cache or to refresh via an adapter. This is what makes the design *one fetch, many subscribers*: Markets, Watchlist, Dashboard, and AI Chat can all subscribe to `market:quote:AAPL` and the hub fans a single producer call out to all of them.
- **`WalletService` legitimately points back at `MarketsScreen`.** This is the documented exception class: **UI-coordinator services** own a modal dialog because the dialog is the user's authority surface (sign transaction, install update). They may accept a `QWidget*` for dialog parenting — a deliberate, scoped exception, not a leak.

Sources: [docs/ARCHITECTURE.md:235-243](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:329-377](docs/ARCHITECTURE.md), [fincept-qt/src/datahub/DataHub.h:43-120](fincept-qt/src/datahub/DataHub.h), [fincept-qt/src/datahub/Producer.h:11-31](fincept-qt/src/datahub/Producer.h)

## Why DataHub Is the Load-Bearing Seam

The `DataHub` class is the *physical* enforcement mechanism for "no cross-context direct calls." Its public surface deliberately offers only three verbs to consumers — `subscribe`, `subscribe_pattern`, `subscribe_errors` — and to producers, only `publish` and `publish_error`. There is no method named `get_quote(symbol)` because that would let a News service call into Markets without going through a topic.

```cpp
// fincept-qt/src/datahub/DataHub.h
QMetaObject::Connection subscribe(
    QObject* owner,
    const QString& topic,
    std::function<void(const QVariant&)> slot);

QMetaObject::Connection subscribe_pattern(
    QObject* owner,
    const QString& pattern,
    std::function<void(const QString&, const QVariant&)> slot);

void publish(const QString& topic, const QVariant& value);
```

Three properties matter for predicting system behavior:

| Property | Consequence |
|---|---|
| `publish()` is safe to call from any thread (queued connection to hub thread) | Producers don't need to know about UI-thread affinity. |
| Subscriptions are owned by a `QObject*` and auto-cancel on `destroyed()` | A screen unloading cannot leak a callback that fires after `delete`. |
| Topic strings encode their shape (`domain:subdomain:id[:modifier]`, e.g. `market:quote:AAPL:1d`) | A topic key is *versioned by its name*; changing the shape means changing the topic. |

Producers are the symmetric side. A new data source implements [Producer](fincept-qt/src/datahub/Producer.h): declare your topic patterns, implement `refresh(topics)`, optionally cap requests-per-second. The hub takes care of scheduling, fan-out, last-known-good on failure, and TTL.

```cpp
// fincept-qt/src/datahub/Producer.h
virtual QStringList topic_patterns() const = 0;          // e.g. {"market:quote:*"}
virtual void refresh(const QStringList& topics) = 0;     // async; calls hub.publish() when done
virtual int max_requests_per_sec() const { return 0; }   // 0 = unlimited
```

If you were to delete DataHub, every cross-context call would have to become either a direct `#include` (breaks the layering) or a typed signal on `EventBus` (which the doc flags as stringly-typed and `O(n)` lookup, with a typed-manifest wrapper planned). DataHub is what lets the monolith *act* modular.

Sources: [fincept-qt/src/datahub/DataHub.h:22-120](fincept-qt/src/datahub/DataHub.h), [fincept-qt/src/datahub/Producer.h:1-31](fincept-qt/src/datahub/Producer.h), [docs/ARCHITECTURE.md:160-167](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:211-216](docs/ARCHITECTURE.md)

## Service Shapes: Three Flavors, Not One

A common misread is to assume *service* means one thing. The architecture explicitly recognises three:

| Flavor | Owns | Returns | Example |
|---|---|---|---|
| **Data service (default)** | One or more DataHub topic patterns | Nothing — publishes to topics | `MarketDataService`, `NewsService`, `EconomicsService` |
| **Imperative service** | A one-shot request/response over HTTP or Python | Signal or `Result<T>` | `MarketSearchService`, `DataMappingTestClient` |
| **UI-coordinator service** | A modal dialog lifecycle on behalf of UI | A signal *plus* may accept `QWidget*` for parenting | `WalletService`, `UpdateService` |

UI-coordinator is the surprising one: it is the *only* sanctioned reason a service may know about `QWidget`. The justification is that the dialog itself is the user's authority surface (wallet connect, sign transaction, install update) — but the business logic still belongs in producers, the dialog is a thin shell over a state machine the service already owns.

Sources: [docs/ARCHITECTURE.md:235-243](docs/ARCHITECTURE.md)

## Predicting Where a New Feature Lives

The whole point of the model is making placement obvious. Worked examples:

| Feature request | Layer | Context | Concrete location |
|---|---|---|---|
| "Add a new India equity quote source" | Application + Adapters | Markets | New `Producer` in `services/markets`, registers `market:quote:*` topics; new adapter under `src/network/` if needed |
| "Add Polymarket prediction screen" | Presentation | Predictions | New screen under `screens/polymarket/`, registered as a lazy factory with `DockScreenRouter`; subscribes to `prediction:*` |
| "Add a 17th broker" | Adapters | Trading | New `brokers/<name>/` directory implementing `BrokerInterface` (target: `BrokerAdapter` base); no service or screen changes |
| "Add a new LLM tool the agents can call" | Adapters | AI Chat | New tool registration in `mcp/McpProvider`; dispatcher handles routing |
| "Add agent crash-resume" | Infrastructure + Application | Agents | Schema migration under `storage/sqlite/migrations/`; `AgentService` writes per-step state to `agent_tasks`; UI subscribes to `agent:<kind>:run:<id>` |
| "Show portfolio P&L on dashboard" | Presentation | Portfolio | New `BaseWidget` subclass under `screens/dashboard/widgets/`; subscribes to `portfolio:*` topic; **no HTTP, no cache, no business logic in the widget** |

Notice that **none** of the rows say "add a new singleton" or "import another context's service header." Both are explicitly listed as anti-patterns.

Sources: [docs/ARCHITECTURE.md:425-447](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:404-422](docs/ARCHITECTURE.md)

## What the Model Buys, and What Would Break If You Changed It

| Property | Why it holds | What breaks if you violate it |
|---|---|---|
| One fetch per (topic, source), shared across all subscribers | DataHub deduplicates subscribers per topic and caches via `CacheManager` | Removing the hub → N screens means N fetches → rate-limit failures on brokers |
| Subscriptions auto-cancel on owner destruction | DataHub holds a `QPointer` owner and listens to `destroyed()` | Bypassing subscribe → callbacks fire on freed memory after screen unload |
| Crash-resume of agents | Per-step state persisted to SQLite *before* publish | Treating DataHub as the only sink → losing in-flight task state on crash |
| Per-OS single binary | Monolith + per-module CMake targets (target state) | Splitting into network services → adds latency, deploy complexity, breaks the "Bloomberg-style desktop" UX intent |
| Provider neutrality (BYOC/BYOK) | LLM, broker, exchange adapters are leaves with no shared dependency on a single vendor SDK | Pulling a vendor SDK into Infrastructure → forces every context to take that dependency |
| 401 → auto-logout for fincept; per-broker for brokers | `SessionGuard` watches fincept HTTP responses; broker adapters own their own refresh | Routing broker HTTP through `SessionGuard` → would log a user out of the app because their Zerodha token expired |

The provider-neutrality point matters specifically for AI / data extensions: because LLM access lives in `services/llm` and tool invocation goes through `mcp/`, a new model provider is a new `ProviderAdapter` shim — no other layer changes. Skill packs and prompt sources stay file/repo/catalog-shaped rather than coupled to one vendor.

Sources: [docs/ARCHITECTURE.md:291-302](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:379-401](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:218-223](docs/ARCHITECTURE.md)

## Summary

The mental model is six layers + thirteen bounded contexts + one in-process pub/sub seam. If you remember only the downward dependency rule and the "cross-context conversations go through DataHub" rule, you can route 90% of new work to the right directory without asking. The CMake layout is moving toward making those rules *mechanically* enforceable; until then, DataHub's deliberately narrow public API and the documented anti-pattern list are what keep the monolith modular.

Sources: [docs/ARCHITECTURE.md:72-78](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:425-447](docs/ARCHITECTURE.md)
