Skip to content

Chapter 2: Object Model Patterns

What Is an Object Model?

An object model is the set of classes, relationships, and conventions that represent a domain in code. In the FCC framework, the object model covers personas, workflows, events, plugins, governance artifacts, and collaboration sessions. Getting the object model right determines how cleanly every other subsystem composes.

This chapter explores the patterns that underpin the FCC object model, using a CTO-level case study to ground the discussion.

The class diagram below sketches the core object-model contracts — DomainEntity, RepositoryProtocol[T], and ModelFacade — and how PersonaRegistry and PersonaSpec implement them.

classDiagram
    class DomainEntity {
        <<Protocol>>
        +entity_id: str
        +entity_type: str
        +namespace: str
    }
    class RepositoryProtocol~T~ {
        <<Protocol>>
        +get(entity_id) T
        +list_all() list~T~
        +search(query) list~T~
        +count() int
    }
    class ModelFacade {
        <<ABC>>
        +stats() dict
        +search(query) list
        +get_full(entity_id) T
    }
    class PersonaRegistry {
        +get(id) PersonaSpec
        +by_category(cat) list
        +champions() list
        +merge(other) PersonaRegistry
    }
    class PersonaSpec {
        +id: str
        +name: str
        +riscear: RISCEARSpec
        +category: str
    }
    RepositoryProtocol~T~ <|.. PersonaRegistry
    DomainEntity <|.. PersonaSpec
    ModelFacade --> RepositoryProtocol~T~ : composes

The same Protocol-plus-Facade pattern recurs throughout the framework for workflows, archetypes, and vocabulary mappings, so learning it here pays off in every later chapter.

The Repository[T] Pattern

At the heart of FCC's data layer is the Repository[T] pattern -- a typed container that owns a collection of domain objects and exposes query, mutation, and serialization operations.

Consider PersonaRegistry:

from fcc.personas.registry import PersonaRegistry

registry = PersonaRegistry.from_yaml_directory("src/fcc/data/personas/")
persona = registry.get("SQC")             # exact lookup
champions = registry.champions()           # filtered query
by_cat = registry.by_category("governance")  # category slice

The registry owns a list[PersonaSpec], indexes it by ID and category on construction, and exposes read-only query methods. The same pattern recurs across the framework:

Registry Element Type T Key
PersonaRegistry PersonaSpec id
WorkflowActionRegistry WorkflowAction (persona_id, action_type)
DimensionRegistry PersonaDimensionProfile category names
CrossReferenceMatrix CrossReferenceEntry (source_id, target_id)
PluginRegistry PluginMeta + instance plugin_id
ConstitutionRegistry PersonaConstitution persona_id

CTO Case Study

Imagine a CTO evaluating FCC for adoption. Their first question is: "How do I find which personas exist, what they do, and how they relate to each other?"

With the Repository pattern, every answer is one method call:

# All personas in the "ml_lifecycle" category
ml_personas = registry.by_category("ml_lifecycle")

# All actions for a specific persona
from fcc.workflow.actions import WorkflowActionRegistry
actions = action_registry.for_persona("SQC")

# All upstream interactions for a persona
from fcc.personas.cross_reference import CrossReferenceMatrix
matrix = CrossReferenceMatrix.from_personas(registry)
upstream = matrix.upstream("SQC")

No SQL. No ORM. No query language. Just typed Python method calls with IDE autocomplete. This is the Repository[T] pattern's strength: it brings the convenience of a database to in-memory domain objects without the abstraction cost.

Protocol vs ABC Design Decision

Python offers two mechanisms for defining contracts: typing.Protocol (structural subtyping) and abc.ABC (nominal subtyping). FCC makes a deliberate, pragmatic choice:

  • ABCs for plugin contracts. Every plugin type has an ABC (PersonaPlugin, EnginePlugin, TemplatePlugin, etc.) defined in fcc.plugins.base. Plugins must inherit from the ABC and implement its abstract methods. This makes validation simple: isinstance(instance, expected_abc).
  • Protocols for internal contracts. The Subscriber type in the event bus is defined as Callable[[Event], Any] -- a structural type, not a class hierarchy. Any callable that accepts an Event is a valid subscriber.

The rationale: plugin boundaries cross package lines, so nominal typing provides clear error messages when a plugin fails to implement a required method. Internal boundaries stay within the FCC package, so structural typing reduces boilerplate.

# Plugin contract — ABC (nominal)
class PersonaPlugin(ABC):
    @abstractmethod
    def plugin_meta(self) -> PluginMeta: ...

    @abstractmethod
    def get_persona_paths(self) -> list[Path]: ...

# Internal contract — Callable (structural)
Subscriber = Callable[[Event], Any]

The ModelFacade Pattern

Individual registries answer questions about their own domain. But cross-cutting questions -- "which governance gates apply to the personas in this workflow graph?" -- require joining data across registries.

The ModelFacade pattern provides a single entry point that composes multiple registries:

class FccModel:
    """Facade over all FCC registries."""

    def __init__(
        self,
        persona_registry: PersonaRegistry,
        action_registry: WorkflowActionRegistry,
        cross_ref: CrossReferenceMatrix,
        dimension_registry: DimensionRegistry,
        plugin_registry: PluginRegistry,
    ):
        self.personas = persona_registry
        self.actions = action_registry
        self.cross_ref = cross_ref
        self.dimensions = dimension_registry
        self.plugins = plugin_registry

    def persona_with_actions(self, persona_id: str):
        """Return a persona and all its registered actions."""
        persona = self.personas.get(persona_id)
        actions = self.actions.for_persona(persona_id)
        return persona, actions

The facade does not own data -- it delegates to the registries it wraps. This keeps each registry independently testable while providing a convenient aggregation point for CLI commands, dashboards, and simulation engines.

Evolution Stages

Object models are not static. The FCC framework recognises four evolution stages, each building on the previous:

Stage 1: Foundational

Plain dataclasses with from_dict / to_dict methods. No indexes, no queries beyond iteration. This is where a new plugin starts.

@dataclass(frozen=True)
class MyModel:
    id: str
    name: str

    @classmethod
    def from_dict(cls, data: dict) -> MyModel:
        return cls(id=data["id"], name=data["name"])

Stage 2: Structured

Add a registry with indexed lookups and filtered queries. The model is now queryable.

class MyRegistry:
    def __init__(self, items: list[MyModel]):
        self._items = list(items)
        self._index = {item.id: item for item in items}

    def get(self, item_id: str) -> MyModel:
        return self._index[item_id]

Stage 3: Semantic

Add cross-references, dimension profiles, and governance metadata. The model captures not just what exists but how things relate and what rules apply.

# Cross-reference: who talks to whom
matrix = CrossReferenceMatrix.from_personas(registry)

# Dimensions: 56-attribute profile per persona
profile = persona.dimension_profile

# Constitution: per-persona governance rules
constitution = constitution_registry.get(persona.id)

Stage 4: Federated

Multiple packages contribute to the same model via the plugin system. The PluginRegistry discovers and merges contributions from any installed package that registers entry points under fcc.plugins.*.

plugin_registry = PluginRegistry()
result = plugin_registry.discover()
# Personas from all discovered plugins are now available
persona_plugins = plugin_registry.get_plugins(PluginType.PERSONAS)

Assessment Methodology

When evaluating an object model (your own or a third party's), apply these questions:

  1. Is every domain concept a frozen dataclass? Immutability prevents a wide class of bugs.
  2. Does every collection have a registry? If you find yourself writing for item in items repeatedly, wrap the list in a registry with named query methods.
  3. Are cross-cutting queries handled by a facade? If a function imports three registries, consider a facade.
  4. Is serialization explicit? from_dict / to_dict is more transparent than implicit serialization frameworks.
  5. Are contracts nominal or structural? Use ABCs at package boundaries, protocols internally.

Best Practices Catalog Reference

The FCC codebase itself serves as a best-practices catalog for the Repository[T] pattern. Key files to study:

File Pattern
src/fcc/personas/registry.py Multi-file YAML loading, by-category indexing, merge
src/fcc/workflow/actions.py Composite key (persona_id, action_type), enum-based type indexing
src/fcc/personas/cross_reference.py Directional queries (upstream, downstream, peers, by_type)
src/fcc/plugins/registry.py Entry-point discovery, ABC validation, type-grouped access
src/fcc/personas/dimensions.py Nested model hierarchy (Profile > Dimension > Attribute)

Key Takeaways

  • The Repository[T] pattern wraps domain object collections in typed, queryable registries.
  • ABCs define plugin contracts (nominal typing); Callables and Protocols define internal contracts (structural typing).
  • The ModelFacade pattern composes registries for cross-cutting queries without duplicating data.
  • Object models evolve through four stages: Foundational, Structured, Semantic, Federated.
  • Frozen dataclasses with explicit from_dict / to_dict are the standard serialization approach.

Previous: Chapter 1 -- Introduction | Next: Chapter 3 -- The R.I.S.C.E.A.R. Specification

Try this in Notebook 02