Skip to content

Modular Architecture

Domain-driven module structure for the platform server, with import rules, ownership conventions, and inter-service communication patterns.

Domain Modules

The server codebase is organized into 7 domain modules. Each domain owns its models, services, API routes, and schemas. Work on any feature must be placed in the correct domain.

Domain Directory UI Section Owns Routes
iam server/src/orchestrator/domains/iam/ IAM + Organization User, Org, Team, Membership, AuthProvider, ServiceToken, Session /api/v1/auth/*, /api/v1/organizations/*, /api/v1/teams/*, /api/v1/iam/*, /api/v1/profile/*
workflow server/src/orchestrator/domains/workflow/ Workflows + Dashboard Workflow, WorkflowRun, Step, Task, RunTask, OrchestratorConfig /api/v1/workflows/*, /api/v1/tasks/*, /api/v1/dashboard/*
catalogue server/src/orchestrator/domains/catalogue/ AI Tool, ToolFile, AIModel, AgentPreset, Prompt, ToolChatMessage /api/v1/tools/*, /api/v1/models/*, /api/v1/agents/*, /api/v1/prompts/*, /api/v1/orchestrators/*, /api/v1/catalogue/*
audit server/src/orchestrator/domains/audit/ Security AuditLog, ViolationLog /api/v1/audit/*
monitoring server/src/orchestrator/domains/monitoring/ Monitoring + Billing ExecutionLog, UsageLog, ObservabilityConfig, Balance /api/v1/logs/*, /api/v1/observability/*
notification server/src/orchestrator/domains/notification/ (cross-cutting) Notification /api/v1/notifications/*
assistant server/src/orchestrator/domains/assistant/ AI Assistant Conversation, Message /api/v1/assistant/*

Domain Directory Structure

Each domain follows this layout:

domains/{domain}/
  __init__.py
  models/          # SQLAlchemy models (one file per model or logical group)
  services/        # Business logic (one file per service class)
  api/             # FastAPI routers (one file per route group)
  schemas/         # Pydantic request/response schemas
  events.py        # Domain event definitions (optional)

Module Boundary Rules

These rules are enforced in every code review

Violations block merge. They exist to keep domains independently extractable as microservices.

Rule 1: Allowed imports

A domain module may ONLY import from:

  • Its own package: from orchestrator.domains.catalogue.models import Tool
  • The core/ package: from orchestrator.core.id_generator import make_id_factory
  • The cwiq-common shared library: from cwiq_common.auth import validate_jwt
  • The contracts/ package for cross-domain DTOs: from orchestrator.contracts.iam import UserDTO

Rule 2: No direct cross-domain imports

# BAD — imports directly from another domain
from orchestrator.domains.iam.models import User

# GOOD — imports a DTO from the contracts package
from orchestrator.contracts.iam import UserDTO

Rule 3: No cross-domain ORM joins

# BAD — joins across domain boundaries
db.query(Workflow).join(Tool)

# GOOD — calls a service that returns DTOs
catalogue_service.get_tools_by_ids(tool_ids)

Rule 4: Ownership columns stay denormalized

owner_org_id, owner_team_id, and created_by_user_id are plain string columns in every domain table. There are no foreign key constraints from these columns to IAM tables. JWT claims provide the values; filtering uses the column directly.

Where to Put New Code

If the feature involves... Domain Reason
User accounts, org settings, team management iam Owns identity and access
Workflow creation, execution, runs, tasks workflow Owns execution lifecycle
Tool/model/agent/prompt CRUD catalogue Owns AI resource registry
Audit trail, security events, compliance audit Owns audit data
Execution logs, usage tracking, billing monitoring Owns observability data
Push notifications, alerts to users notification Owns notification delivery
AI chat, context building assistant Owns conversation state
Cross-cutting utilities (auth, config, DB) core Shared infrastructure
Cross-domain data shapes contracts DTOs and Protocol types

Prefixed IDs

All resource IDs use the format {prefix}-{16 hex chars}:

org-a3f8e21b4c7d9012
wf-7b2e4f1a8c3d5069

Use make_id_factory for all model primary key defaults:

from orchestrator.core.id_generator import make_id_factory

class Workflow(Base):
    id: Mapped[str] = mapped_column(
        String(26), primary_key=True, default=make_id_factory("wf")
    )

Never use uuid.uuid4() directly for resource IDs

Always use make_id_factory("prefix"). The prefix registry is in platform/plans/multi-tenancy/PREFIXED_IDS.md.

make_id_factory is also available in cwiq_common.models (cwiq-common v0.1.1+).

Inter-Service Communication

Pattern Use For Technology
Synchronous HTTP CRUD reads, validation on write nginx proxy between services
Kafka events Audit trail, usage tracking, notifications aiokafka + Redpanda
Redis pub/sub Real-time WebSocket events (1–5ms) Redis channels
In-process function call Same-domain operations Direct Python call
JWT validation Auth on every request cwiq-common (in-process, no network hop)
Aggregated health fan-out Liveness across all microservices (?detailed=true) server fans out via httpx to each /health

Microservices Extraction Roadmap

Each domain is designed for independent extraction. When a domain is extracted: - Internal function calls become HTTP or Kafka calls - contracts/ DTOs define the API boundary

See platform/plans/micro-services/ for the full decomposition roadmap.