Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Decision records

Explanation

EventBus-only inter-module communication

Decision record for the rule that Cluster ERP modules never import each other and communicate only through EventBus events and queries.

Status: Accepted, stated in the project's written engineering rules. This record covers the rule that a module of Cluster ERP never imports another module and reaches other modules only through the EventBus in src/cluster_erp/core/event_bus.py, with contracts from src/cluster_erp/core/contracts.py. The mechanism itself is explained on event bus.

Context

Cluster ERP is split into modules under src/cluster_erp/modules/, 28 of them counted as package folders. Each module is loaded on its own by the registry and may be enabled or not (src/cluster_erp/core/module_registry.py:58-72). The project's written engineering rules forbid direct communication between modules and call it a mandatory architecture rule. The boundary test states the intent: the rules are "held by a test instead of by memory" (tests/test_architecture_boundaries.py:1-11). Rationale beyond keeping modules independent is not determined from code.

Decision

  • A module never imports another module. The single sanctioned import is cluster_erp.modules.auth.guards, which supplies the user type and the permission dependencies (tests/test_architecture_boundaries.py:3-7, tests/test_architecture_boundaries.py:24).
  • Everything else goes through the EventBus: an Event is fire-and-forget with any number of handlers; a Query is request and response with exactly one handler (src/cluster_erp/core/event_bus.py:43-49).
  • Contracts are frozen dataclasses in src/cluster_erp/core/contracts.py.
  • The application layer in src/cluster_erp/server/routes/ is not a module. It may orchestrate several module services in one request (src/cluster_erp/server/routes/party_composite.py:4-9).

The sequence diagram shows the two patterns.

sequenceDiagram
 participant A as Module A service
 participant Bus as EventBus
 participant B as Module B handler
 participant C as Module C handler
 A->>Bus: emit(Event)
 Bus->>B: handler(event), by priority
 Bus->>C: handler(event), by priority
 A->>Bus: query(Query)
 Bus->>B: the single registered handler
 B-->>A: result

In prose: emit calls every subscribed handler of the event's type, ordered by EventPriority, and returns their results (src/cluster_erp/core/event_bus.py:55-65, src/cluster_erp/core/event_bus.py:83-96). query calls the one handler registered for the query's type and returns its answer (src/cluster_erp/core/event_bus.py:98-102).

Consequences

  • src/cluster_erp/core/contracts.py holds 146 classes that subclass Event and 118 that subclass Query (counted with grep -c).
  • An exception in an event handler is logged and does not reach the sender; the other handlers still run (src/cluster_erp/core/event_bus.py:91-95).
  • A query without a handler raises QueryHandlerNotFoundError (src/cluster_erp/core/event_bus.py:98-101). A second handler for the same query raises DuplicateQueryHandlerError at registration (src/cluster_erp/core/event_bus.py:78-79).
  • Handlers that run inside a request use request_database() so they read the current tenant's database (src/cluster_erp/core/module_base.py:40-59).
  • Validation that spans modules is a query. Example: ensure_product_breakdown sends ValidateProductBreakdown, which products_and_services answers (src/cluster_erp/core/product_breakdown.py:11-13, src/cluster_erp/modules/products_and_services/module.py:131).
  • The desktop client follows the same idea with its own signal bus; see signal bus and watch_entity.

Where the code enforces it

Mechanism What it checks Source
test_no_module_imports_another_module_except_through_the_guards_door Every import in every module file tests/test_architecture_boundaries.py:46-57
test_modules_exist The scan covers at least 20 modules tests/test_architecture_boundaries.py:42-43
EventBus.register_query_handler One handler per query type src/cluster_erp/core/event_bus.py:73-81
CI test job Runs the boundary test on every push .github/workflows/ci.yml:124

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13