Skip to content

Domain Models and Lifecycle Binding

This guide explains how to use PyCharter contracts as domain entity models with an optional lifecycle binding: a convention that links an entity type to a state machine (e.g. for order management or trading sessions) without PyCharter depending on any FSM engine. When you use an FSM engine such as PyStator, you can resolve the contract, read the binding, validate payloads, and call the engine seamlessly.

Table of Contents

  1. What is a domain entity contract?
  2. The lifecycle convention
  3. Reading the binding in code
  4. Plugging into PyStator
  5. Seed domain contracts

What is a domain entity contract?

A domain entity contract in PyCharter is a data contract that describes a "noun" in your domain (e.g. Order, TradingSession, RebalanceRun). It has:

  • A schema: the shape of the entity (id, status, and other fields).
  • Optional lifecycle metadata: which state machine governs this entity’s lifecycle (e.g. order_lifecycle, intraday_signal_lifecycle, optimization_cycle).

The contract does not store entity instances; it only stores the definition. Entity state (current FSM state per instance) lives in your application or in an FSM engine such as PyStator.


The lifecycle convention

A domain entity contract may include an optional lifecycle block in metadata at the root (metadata.lifecycle) or under governance_rules (metadata.governance_rules.lifecycle). PyCharter reads either location. The following fields align with FSM engines such as PyStator (machine name/version, entity id and state field mapping):

Key Required Description
state_machine_name Yes Name of the FSM that governs this entity (e.g. order_lifecycle). Must match the FSM’s meta.machine_name (e.g. PyStator trading_machines).
machine_version No FSM machine version (e.g. "1.0.0"). If omitted, the engine typically uses the latest version. PyStator API accepts machine_version in the request body.
state_field No Name of the field on the entity that holds the current FSM state. Default: "status". Schema enum for this field should match the FSM state names.
entity_id_field No Name of the field on the entity used as entity_id in FSM APIs (e.g. order_id, cycle_id). Used when calling e.g. POST /entities/{entity_id}/events.

Example in YAML (root-level lifecycle, as in the metadata template):

lifecycle:
  state_machine_name: order_lifecycle
  machine_version: "1.0.0"
  state_field: status
  entity_id_field: order_id

For the app and the FSM engine to stay in sync, the schema’s status (or state_field) enum should match the state names of the corresponding FSM. When the engine returns a new state, write it back to the entity’s state field. Use entity_id_field to know which field value to pass as entity_id to the FSM API.


Reading the binding in code

PyCharter provides engine-agnostic helpers that operate on in-memory dicts (no FSM dependency).

get_lifecycle_binding(metadata)

Returns the full lifecycle dict from metadata.lifecycle (root) or metadata["governance_rules"]["lifecycle"] if present and valid (must contain state_machine_name); otherwise None. The dict may include machine_version and entity_id_field when defined in metadata.

from pycharter import get_lifecycle_binding

metadata = {
    "lifecycle": {
        "state_machine_name": "order_lifecycle",
        "machine_version": "1.0.0",
        "state_field": "status",
        "entity_id_field": "order_id",
    }
}
binding = get_lifecycle_binding(metadata)
# binding has state_machine_name, state_field, machine_version, entity_id_field

get_domain_entity_info(contract_dict)

Given a full contract dict (e.g. from a store or build_contract), returns state_machine_name, state_field (default "status"), and when present in metadata: machine_version, entity_id_field. Use entity_id = record[info["entity_id_field"]] when calling the FSM API, and pass machine_version if you need to pin the FSM version.

from pycharter import get_domain_entity_info

contract = {
    "metadata": {
        "lifecycle": {
            "state_machine_name": "order_lifecycle",
            "entity_id_field": "order_id",
        }
    },
    "schema": { ... },
}
info = get_domain_entity_info(contract)
# info == {"state_machine_name": "order_lifecycle", "state_field": "status", "entity_id_field": "order_id"}

Plugging into PyStator

When both PyCharter and PyStator are used, a typical flow is:

  1. Resolve the domain contract from PyCharter (by name/version from your store or API).
  2. Read the lifecycle binding with get_lifecycle_binding(metadata) or get_domain_entity_info(contract_dict) to get state_machine_name, state_field, and optionally machine_version and entity_id_field.
  3. Validate the payload (e.g. order or event context) with PyCharter’s Validator against the contract’s schema.
  4. Call PyStator with:
  5. entity_id: from your record (e.g. record[info["entity_id_field"]] when entity_id_field is set),
  6. machine_name: info["state_machine_name"] (must match PyStator meta.machine_name),
  7. optional machine_version: info.get("machine_version") to pin FSM version,
  8. trigger: event name,
  9. context: validated payload or subset.

Example (conceptual):

# 1. Load contract from store or file
contract = build_contract_from_store(store, "order_domain", "1.0.0")

# 2. Get lifecycle binding (no PyStator import in PyCharter)
info = get_domain_entity_info(contract)
if not info:
    raise ValueError("Contract has no lifecycle binding")

# 3. Validate payload
validator = Validator.from_dict(contract["schema"], ...)
validator.validate(payload)

# 4. Call PyStator (your code or HTTP client)
# entity_id = payload.get(info.get("entity_id_field", "id"))
# POST /entities/{entity_id}/events
# body: { "trigger": "exchange_ack", "machine_name": info["state_machine_name"],
#         "machine_version": info.get("machine_version"), "context": payload }

A runnable example is provided under examples/domain_lifecycle_pystator_example.py (or in docs examples).


Seed domain contracts

The PyCharter seed data includes domain contracts for intraday trading and weekly portfolio optimization. Each binds to a PyStator FSM by name (see pystator seed: trading_machines.yaml).

Contract name Purpose FSM (state_machine_name)
order_domain Single order lifecycle (intraday or rebalance) order_lifecycle
intraday_signal_domain One signal: detect → validate → submit → fill/cancel intraday_signal_lifecycle
intraday_session_domain Trading session: pre_market → … → post_market intraday_trading_session
portfolio_domain Portfolio lifecycle: monitoring → rebalancing → review / suspended portfolio_lifecycle
optimization_cycle_domain One optimization run: data_collection → optimizing → executing → completed optimization_cycle
flight_domain Flight lifecycle: scheduled → boarding → departed → in_flight → arrived / cancelled flight_lifecycle

After running pycharter db seed, these contracts and their schemas, metadata (with lifecycle), coercion/validation rules, and optionally ontologies (PostgreSQL Wiki) are loaded. Use them to validate entity payloads and to obtain state_machine_name when calling PyStator.


See also

  • Concepts – Data contracts, validation pipeline, metadata store.
  • Contract Builder API – Building and resolving contracts.
  • Example: examples/domain_lifecycle_pystator_example.py (if present in the repo) – Load contract → lifecycle → validate → call PyStator.