Design & Architecture: Under the Hood¶
This document explains DxMessaging’s internal design, performance optimizations, and architectural decisions. Read this to understand how and why DxMessaging works the way it does.
Table of Contents¶
- Core Design Principles
- Architecture Overview
- Performance Optimizations
- Message Type System
- Registration and Lifecycle
- The Message Bus
- Why DxMessaging is Fast
- Design Decisions and Tradeoffs
Core Design Principles¶
DxMessaging was built with these principles:
- Zero‑Allocation Communication
- Messages are
readonly structtypes passed byref. - No boxing, no temporary objects, minimal GC pressure.
-
Handlers receive
refparameters for struct messages. -
Type‑Safe by Default
- Compile‑time guarantees via generic constraints.
- No string‑based dispatch (unlike Unity’s
SendMessage). -
Source generators provide boilerplate‑free message definitions.
-
Predictable Execution
- Priority‑based handler ordering (lower priority runs first).
- Three‑stage pipeline: Interceptors → Handlers → Post‑Processors.
-
Deterministic behavior within each priority level.
-
Observable & Debuggable
- Built‑in diagnostics via
CyclicBuffer. - Registration logging with
RegistrationLog. -
Inspector integration for runtime visibility.
-
Lifecycle Safety
MessageRegistrationTokenmanages enable/disable.- Automatic cleanup prevents memory leaks.
-
Unity lifecycle integration via
MessageAwareComponent. -
Decoupled by Nature
- Three semantic categories: Untargeted, Targeted, Broadcast.
- No direct references between producers and consumers.
- Context‑aware (who sent, who received) without tight coupling.
Architecture Overview¶
flowchart TB
subgraph App["Application Layer"]
CompA[Component A<br/>Token.Reg]
CompB[Component B<br/>Token.Reg]
CompC[Component C<br/>Token.Reg]
end
subgraph Token["Registration Layer"]
MRT[MessageRegistrationToken<br/>• Stages registrations<br/>• Enable/Disable<br/>• Lifecycle management]
end
subgraph Handler["Handler Layer"]
MH[MessageHandler<br/>• Per-component handler<br/>• Active/Inactive state]
end
subgraph Bus["Message Bus Layer"]
MB[MessageBus<br/>• Interceptors<br/>• Handlers<br/>• Post-Processors]
end
CompA ==> MRT
CompB ==> MRT
CompC ==> MRT
MRT ==> MH
MH ==> MB
classDef primary stroke-width:3px
class App,CompA,CompB,CompC primary
classDef warning stroke-width:3px
class Token,MRT warning
classDef accent stroke-width:3px
class Handler,MH accent
classDef success stroke-width:3px
class Bus,MB success Layer Responsibilities¶
- Application Layer - Your Unity components register message handlers
- Registration Layer - Token manages handler lifecycle (enable/disable/cleanup)
- Handler Layer - Per-component state management (active/inactive)
- Message Bus Layer - Routes messages through interceptors → handlers → post-processors
Performance Optimizations¶
- Struct messages passed by
refto avoid copying and GC. - Minimal allocations in hot paths; logging and diagnostics use ring buffers.
- Pre‑allocated internal collections for common operations.
- Handlers are sorted by priority once during registration. Emitting a message iterates through all active handlers in that order.
Message Type System¶
- Untargeted: broadcast‑like notifications without an explicit receiver.
- Targeted: deliver to a specific target (e.g., GameObject,
InstanceId). - Broadcast: deliver to all listeners (optionally capturing the source).
Attributes like [DxTargetedMessage] and [DxBroadcastMessage] (with source generators) provide strong typing with minimal boilerplate.
Registration and Lifecycle¶
MessageRegistrationTokengroups per‑component registrations.- Enable/disable toggles all component handlers together.
- Disposal cleans up handlers automatically, preventing leaks.
MessageAwareComponentwires Unity lifecycles to tokens for safety.
The Message Bus¶
Message flow: Interceptors → Handlers → Post‑Processors.
- Interceptors may transform or cancel messages before delivery.
- Handlers execute in priority order; lower number executes first.
- Post‑processors observe outcomes and can emit follow‑up messages.
Why DxMessaging is Fast¶
- No reflection for dispatch; compile‑time generics and static typing.
- No string dispatch or dynamic lookup.
- Ref‑based delivery avoids copies and allocations.
- Tight internal data structures tuned for Unity hot loops.
Design Decisions and Tradeoffs¶
- Priorities are numeric for clarity and control; predictable ordering beats implicit timing.
- Strong typing over dynamic flexibility; safer refactoring and IDE support.
- Diagnostics are opt‑in and lightweight to keep runtime overhead minimal.
See also: