Domain Models and Lifecycle - PyCharter
Description: Data Contract Management, ETL Pipelines, and Quality Assurance for Python

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.