Skip to content

Currency / Bootstrap

Purpose

The Bootstrap layer composes and publishes a Currency service stack for resolution.

It defines how a service is:

  • Discovered (scene-local)
  • Composed (decorators)
  • Published (resolver override)

This layer controls wiring and lifecycle, not behaviour.


Usage Guidance

Bootstrap is responsible for:

  • Composing a currency service stack from building blocks
  • Publishing the composed service via CurrencyResolve
  • Managing scoped lifecycle for resolver overrides
  • Providing a default composition pipeline

What Lives Here

CurrencyBootstrap

Low-level publishing helper.

public static IDisposable Publish(ICurrencyService service)
  • Sets the active resolver override
  • Returns a scope that clears it on Dispose()

CurrencyServiceBootstrap

Scene component that builds and publishes the service stack.

Execution order:

[DefaultExecutionOrder(-499)]

Behaviour:

  • On Start():

  • Finds a SceneCurrencyService

  • Warns if no ICurrencyAuthority is present
  • Composes the service stack
  • Publishes via CurrencyBootstrap

  • On OnDisable / OnDestroy:

  • Disposes the scope (clears override)

While nothing is published, resolution is ungated

CurrencyResolve falls back to the raw SceneCurrencyService whenever the override slot is empty: before Start on the first frame, for as long as the bootstrap is disabled, and after it is destroyed. The raw service has no authority, no caps, no audit and no idempotency, and it refuses nothing — Currency's fail-closed guarantee is scoped to "when using an authority-wrapped service", and this is the window in which you are not.

Disable the bootstrap only when no mutation can run. A loading screen is fine; a disabled bootstrap in a live scene is an open wallet.

Two publishers: last wins, and the first dispose clears the slot for both

There is one override slot. A second Publish displaces the first, and whichever scope is disposed first empties the slot — so tearing down bootstrap B leaves bootstrap A alive, enabled, and no longer published, with every later resolve landing on the raw service. Nothing republishes on A's behalf, because nothing tracks who owns the slot.

Publishing over a live override logs a warning in the editor naming both stacks. That is the signal you are in this shape — and it is #if UNITY_EDITOR, so a player build and a dedicated server get no signal at all. Where the failure shows up is a silently ungated wallet.

The rule is once per process, not once per scene. Step 0 of resolution is the published override, checked before any scene is consulted, so the slot is process-global. Publishing once per scene in a two-scene process produces exactly the two publishes this warning is about — two additively loaded worlds, or two matches on one dedicated server, and every resolve in both returns whichever published last: its authority context, its caps, its audit ring, its idempotency window.

So the composition model is one simulated world per process. If you need two, the second needs its own process. Authority resolution is genuinely per-scene (CurrencyAuthority keys its cache by Scene); it is service publication that is not. CurrencyBootstrapOneWorldPerProcessTests pins this, and no shipped sample scene has two.


Important Notes

  • Bootstrap does not implement business or mutation logic
  • Behaviour is defined by the composed service stack
  • Authority must be present when authority decorators are composed
  • Resolver overrides are global and should be scoped carefully
  • Multiple bootstraps require intentional ordering

Usage Guidance

Resolver override

Currency resolution is performed via CurrencyResolve.

Publishing a service ensures that all consumers resolve the same active instance.

The override remains active until the returned scope is disposed.


Scoped publishing

var scope = CurrencyBootstrap.Publish(service);
  • The service becomes the active resolver override
  • Disposing the scope clears the override — any scope, including one whose service a later publish has already replaced. The slot has no owner

Default composition pipeline

The built-in bootstrap composes:

SceneCurrencyService
→ Caps + Audit + Authority
→ Idempotency
→ BatchEvents

RequireEscrow may also be included depending on policy.

Order is intentional:

  • Caps run before audit
  • Authority gates mutations
  • Idempotency wraps outer mutations
  • Batch emits grouped events

Custom bootstrap example

public class MyBootstrap : MonoBehaviour
{
    private IDisposable _scope;

    void Awake()
    {
        var inner = FindFirstObjectByType<SceneCurrencyService>();
        if (!inner) return;

        ICurrencyService svc = inner;

        svc = CurrencyFactories.WithAudit(svc);
        svc = CurrencyFactories.WithCaps(svc, policy);
        svc = CurrencyFactories.WithAuthority(svc, this);

        _scope?.Dispose();
        _scope = CurrencyBootstrap.Publish(svc);
    }

    void OnDisable()
    {
        _scope?.Dispose();
        _scope = null;
    }
}

Always compose from the inner service, not from CurrencyResolve, to avoid stacking decorators multiple times.


Internal Use Only

This folder handles composition and publishing only.

Do not:

  • Add gameplay logic
  • Implement mutation rules here
  • Depend on resolver overrides for application logic

Safe to Remove

This folder may be replaced if a custom bootstrap solution is used.

Removing it requires providing an alternative service composition and publishing strategy.


  • Abstractions — core contracts
  • Internal — decorator implementations
  • Authority — mutation gating
  • Policies — caps and transfer rules
  • UnityIntegration — SceneCurrencyService