PSP follows an atomic design-inspired composition model for server-side components. Lower-level components can be composed into higher-level ones through direct injection—passing component instances as constructor parameters.

Levels of Composition

Level Name Definition Example
Atom Use Case Single operation with dependencies CreateTask, ScheduleTask
Molecule Component Group of related use cases TodosComponent, BudgetComponent
Organism Composite Wraps multiple components ScheduledTodosComponent

Direct Injection Pattern

Composite components receive lower-level components via constructor injection. They don't create their dependencies internally—they receive them from above.

component.py
class ScheduledTodosComponent:
    """Composite component that wraps Todos and Budget."""

    def __init__(
        self,
        todos: TodosComponent,      # Injected molecule
        budget: BudgetComponent,    # Injected molecule
        clock: Clock,
        event_bus: EventBus,
    ) -> None:
        self._todos = todos
        self._budget = budget

        # Wire composite use cases that orchestrate both
        self.create_scheduled_task = CreateScheduledTask(
            create_task=todos.create_task,
            delete_task=todos.delete_task,
            schedule_task=budget.schedule_task,
            clock=clock,
            event_bus=event_bus,
        )
Key Characteristics
  • Explicit dependencies — Lower-level components are constructor parameters
  • No internal creation — Composite doesn't instantiate its dependencies
  • Shared services — Clock and EventBus are shared for cross-component communication
  • Use case delegation — Composite use cases call lower-level use cases

Shared Platform Services

For components to communicate, they must share platform services. When events published by one component need to be seen by another, both must use the same EventBus.

factory.py
# Shared services enable cross-component communication
clock = SystemClock()
event_bus = InMemoryEventBus()

# Both components share the same event bus
todos = create_todos_component(clock=clock, event_bus=event_bus)
budget = create_budget_component(clock=clock, event_bus=event_bus)

# Composite wraps them
scheduled = ScheduledTodosComponent(todos, budget, clock, event_bus)

Composite Use Case Structure

A composite use case orchestrates multiple lower-level use cases. It may also implement compensating transactions for error recovery.

create_scheduled_task.py
class CreateScheduledTask:
    """Create task AND schedule budget atomically."""

    def __init__(
        self,
        create_task: CreateTask,      # From Todos
        delete_task: DeleteTask,      # From Todos (for rollback)
        schedule_task: ScheduleTask,  # From Budget
        clock: Clock,
        event_bus: EventBus,
    ) -> None:
        ...

    def execute(self, actor_id: UUID, input: Input) -> Output:
        # 1. Create task via Todos
        task = self._create_task.execute(...)

        try:
            # 2. Schedule budget via Budget
            scheduled = self._schedule_task.execute(...)
        except InsufficientBudgetError:
            # Compensating transaction
            self._delete_task.execute(...)
            raise

        # 3. Publish composite event
        self._event_bus.publish(ScheduledTaskCreated(...))

        return Output(task=task, scheduled=scheduled)

Factory Functions

Each level provides a factory function with sensible defaults. The organism-level factory creates and wires the molecules automatically.

scheduled_todos/component.py
def create_scheduled_todos_component(
    storage: Literal["memory", "sqlite"] = "memory",
    clock: Clock | None = None,
    event_bus: EventBus | None = None,
) -> ScheduledTodosComponent:
    shared_clock = clock or SystemClock()
    shared_event_bus = event_bus or InMemoryEventBus()

    # Create molecules with shared services
    todos = create_todos_component(clock=shared_clock, event_bus=shared_event_bus)
    budget = create_budget_component(clock=shared_clock, event_bus=shared_event_bus)

    return ScheduledTodosComponent(todos, budget, shared_clock, shared_event_bus)

When to Create a Composite

Create a composite component when:

  • Atomic operations span multiple components — e.g., create task AND allocate budget
  • Business logic ties components together — e.g., completing a task should spend budget
  • You want a unified API — simpler interface than calling both components separately
  • Cross-component invariants exist — e.g., every scheduled task must have a todo
Reference Implementation

See src/psp/components/scheduled_todos/ for a complete example of composite component, composite use cases, composite events, factory function, and catalogue registration.

Directory Structure

A composite component follows the same structure as a molecule:

structure
src/psp/components/scheduled_todos/
├── __init__.py              # Public API exports
├── component.py             # Composition root
├── module.py                # Catalogue registration
├── domain/
│   ├── events.py            # Composite events
│   └── errors.py            # Composite errors
└── application/
    ├── dto.py               # Input/Output objects
    └── use_cases/
        ├── create_scheduled_task.py
        └── complete_scheduled_task.py