Skip to content

Best Practices

Rules of thumb for structuring hexa pipelines that stay easy to extend and verify. Grounded in the extraction pipeline sample and the test suite.


๐ŸŒณ Layer Discipline

  • Never subclass an ABC for a bank. Bank classes always subclass the concrete Impl* classes. The ABC defines the contract once; Impl* carries behavior; banks only vary.
  • Pin concrete types at Impl*, not at the ABC. The ABC declares shape (number: NumberPort); Impl* decides implementation (number: ImplNumberPort).
  • Keep the root thin. A bank root is one annotated subclass:
    Python
    class AxisExtractionPipeline(ImplExtractionPipeline):
        txn_dicts: AxisTxnDictsPort
    

๐Ÿ‘ฃ Only Redeclare What You Change

The redundancy rule: if a slot's behavior is already correct from Impl*, leave it out.

  • date is not redeclared on AxisTransactionParserPort โ€” it inherits ImplDatePort.
  • ambiguity is not redeclared on AxisAmountBalancePort โ€” it inherits ImplAmbiguityPort.
  • Re-declaring slots you don't change adds mental load on the extender with zero behavior change โ€” and keeps the diff between a bank and Impl* minimal.

If you do redeclare a slot, re-pin it back to your own bank's port (e.g. amount_balance.number: AxisNumberPort) so a single bank's number formatting is consistent down the subtree.


๐Ÿ“„ Representational Fidelity

Keep YAML and ABC in agreement:

  • Every converter (parse_yaml, generate_abc, parse_abc, generate_yaml) serializes through the same PortNode tree โ€” any tree fits, nothing is tied to a particular sample.
  • Use check_matches(spec.yaml, abc.py) (or hexa check ...) as a CI guard whenever both representations are maintained.
  • Class-level attributes on ABCs are used as documentation of the config surface; pin defaults at the Impl* level so the ABC stays a pure contract.
  • Nested grouped values are expressed once per representation: a @dataclass annotation (ABC) โ‡„ a nested struct mapping (YAML).

๐Ÿงฉ Wiring Discipline

  • Put plain runtime objects that must not be re-created (repos, handlers, clients) in instances= โ€” wired verbatim by name, never reconstructed.
  • Put plain config values in config= โ€” propagated by name across the whole tree, including nested ports.
  • Name a slot whatever the annotated field is called; build matches instances/config by that annotated name, so names must agree across the tree.
  • Keep port slot annotations to concrete Impl*/bank classes (or ABCs when the leaf is a config-only option). Builtin/typing/union-annotated fields are treated as config, not ports โ€” so don't annotate a port with # type: str.

๐Ÿงช Test Layout

Mirror tests/:

  • One test module per component (test_parse_yaml.py, test_parse_abc.py, test_generate_abc.py, ...).
  • A dedicated test_container.py for build behavior (injection semantics, config propagation, defaults).
  • Use samples/minimal as the smallest contrived base fixture: one root, one nested port, a few config vars, a single abstract method. It bikes the whole YAML โ‡„ ABC โ‡„ runtime loop in a few lines.
  • Test the round-trip property directly โ€” parsed models from both representations must be equal (node1.diff(node3) == []) and regeneration must be idempotent.