Skip to content

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

DxMessaging was built with these principles:

  1. Zero‑Allocation Communication
  2. Messages are readonly struct types passed by ref.
  3. No boxing, no temporary objects, minimal GC pressure.
  4. Handlers receive ref parameters for struct messages.

  5. Type‑Safe by Default

  6. Compile‑time guarantees via generic constraints.
  7. No string‑based dispatch (unlike Unity’s SendMessage).
  8. Source generators provide boilerplate‑free message definitions.

  9. Predictable Execution

  10. Priority‑based handler ordering (lower priority runs first).
  11. Three‑stage pipeline: Interceptors → Handlers → Post‑Processors.
  12. Deterministic behavior within each priority level.

  13. Observable & Debuggable

  14. Built‑in diagnostics via CyclicBuffer.
  15. Registration logging with RegistrationLog.
  16. Inspector integration for runtime visibility.

  17. Lifecycle Safety

  18. MessageRegistrationToken manages enable/disable.
  19. Automatic cleanup prevents memory leaks.
  20. Unity lifecycle integration via MessageAwareComponent.

  21. Decoupled by Nature

  22. Three semantic categories: Untargeted, Targeted, Broadcast.
  23. No direct references between producers and consumers.
  24. 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

  1. Application Layer - Your Unity components register message handlers
  2. Registration Layer - Token manages handler lifecycle (enable/disable/cleanup)
  3. Handler Layer - Per-component state management (active/inactive)
  4. Message Bus Layer - Routes messages through interceptors → handlers → post-processors

Performance Optimizations

  • Struct messages passed by ref to 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

  • MessageRegistrationToken groups per‑component registrations.
  • Enable/disable toggles all component handlers together.
  • Disposal cleans up handlers automatically, preventing leaks.
  • MessageAwareComponent wires 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: