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 infcc.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
Subscribertype in the event bus is defined asCallable[[Event], Any]-- a structural type, not a class hierarchy. Any callable that accepts anEventis 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:
- Is every domain concept a frozen dataclass? Immutability prevents a wide class of bugs.
- Does every collection have a registry? If you find yourself writing
for item in itemsrepeatedly, wrap the list in a registry with named query methods. - Are cross-cutting queries handled by a facade? If a function imports three registries, consider a facade.
- Is serialization explicit?
from_dict/to_dictis more transparent than implicit serialization frameworks. - 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_dictare 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