Skip to content

System Architecture: Phoenix ML Platform

1. Overview

Phoenix ML is an end-to-end MLOps system built with Domain-Driven Design (DDD) and Clean Architecture. It provides:

  • Real-time ML inference (REST + gRPC)
  • Automatic data drift and anomaly detection
  • Self-healing: alert → rollback → retrain
  • Multi-model serving with A/B testing and canary deployment
  • Full observability stack (metrics, tracing, logging)

2. Architecture Diagram

2.1 Overall System Architecture

graph TD
    Client["External Client"] -->|REST/gRPC| API["Phoenix API Gateway<br/>FastAPI + gRPC"]

    subgraph "Inference Core"
        API --> Handler["PredictHandler<br/>CQRS Command Handler"]
        Handler --> Pipeline["Request Pipeline<br/>Chain of Responsibility"]
        Pipeline --> Router["Routing Strategy<br/>A/B · Canary · Shadow"]
        Router --> CB["Circuit Breaker<br/>Closed/Open/Half-Open"]
        CB --> Engines

        subgraph Engines["Model Executors"]
            ONNX["ONNX Runtime<br/>(Default - CPU/GPU)"]
            TRT["TensorRT Executor<br/>(FP16 GPU)"]
            Triton["Triton Client<br/>(HTTP v2 API)"]
        end
    end

    subgraph "Data Layer"
        Handler -->|"HMGET/HSET"| Redis["Redis<br/>(Online Feature Store)"]
        Handler -->|Read| Parquet["Parquet Files<br/>(Offline Features)"]
        API -->|"SQLAlchemy async"| Postgres["PostgreSQL<br/>(Models, Logs, Reports)"]
        API -->|"boto3"| MinIO["MinIO / S3<br/>(Model Artifacts)"]
    end

    subgraph "Event Streaming"
        Handler -.->|"JSON publish"| Kafka{"Apache Kafka<br/>(KRaft mode)"}
        Kafka -->|"Consumer group"| Logger["Prediction Logger"]
        Kafka -->|"Consumer group"| Analytics["Stream Analytics"]
    end

    subgraph "Self-Healing Subsystem"
        Monitor["MonitoringService<br/>(Background loop)"] --> Drift["DriftCalculator<br/>(KS · PSI · Chi2)"]
        Monitor --> Anomaly["AnomalyDetector<br/>(Z-score · IQR)"]
        Monitor --> Alert["AlertManager<br/>(Rules + Cooldown)"]
        Alert --> Notifier["AlertNotifier<br/>(Slack / Discord webhook)"]
        Alert --> Rollback["RollbackManager<br/>(Auto-revert model)"]
    end

    subgraph "Observability"
        API -->|"/metrics"| Prometheus["Prometheus<br/>(Scrape metrics)"]
        Prometheus --> Grafana["Grafana<br/>(Dashboards)"]
        API -->|"OTLP export"| Jaeger["Jaeger<br/>(Distributed Tracing)"]
    end

    subgraph "Frontend"
        Dashboard["React Dashboard<br/>(TypeScript + Recharts)"]
        Dashboard -->|"fetch API"| API
        Dashboard -->|"iframe embed"| Grafana
    end

    subgraph "MLOps Pipeline"
        Airflow["Apache Airflow<br/>(Scheduler)"] -->|"DAG tasks"| Train["Train Models<br/>(sklearn → ONNX)"]
        Train -->|"MLflow log"| MLflow["MLflow<br/>(Experiment Tracking)"]
    end

2.2 Request Flow (Single Prediction)

sequenceDiagram
    participant C as Client
    participant API as FastAPI Router
    participant PH as PredictHandler
    participant FS as FeatureStore
    participant IS as InferenceService
    participant RS as RoutingStrategy
    participant CB as CircuitBreaker
    participant IE as ONNX Engine
    participant EB as EventBus
    participant KF as Kafka
    participant PG as PostgreSQL

    C->>API: POST /predict {model_id, features}
    API->>PH: handle(PredictCommand)

    opt entity_id provided (no raw features)
        PH->>FS: get_online_features(entity_id)
        FS-->>PH: [0.5, 1.2, 0.8, ...]
    end

    PH->>IS: predict(model, features)
    IS->>RS: select_model(champion, challenger)
    RS-->>IS: selected_model (based on strategy)
    IS->>CB: check_state()

    alt Circuit CLOSED
        IS->>IE: predict(model, feature_vector)
        IE-->>IS: Prediction(result, confidence, latency)
    else Circuit OPEN
        IS-->>PH: raise CircuitOpenError
    end

    IS-->>PH: Prediction

    par Async Task 1
        PH->>EB: publish(PredictionCompleted)
    and Async Task 2
        EB->>KF: publish("inference-events", event)
    and Async Task 3
        PH->>PG: log_prediction(command, prediction)
    end

    PH-->>API: PredictionResponse
    API-->>C: 200 OK {result, confidence, latency_ms}

3. Layer Architecture

Phoenix ML uses a layered architecture — dependency rule: outer layers depend on inner layers, never the reverse.

graph TD
    subgraph Infrastructure
        I["FastAPI · gRPC · PostgreSQL · Redis · Kafka<br/>ONNX Runtime · Prometheus · S3 · MLflow"]
    end
    subgraph Application
        A["PredictHandler · BatchPredictHandler<br/>MonitoringService · Commands · Queries · DTOs"]
    end
    subgraph Domain
        D["Model · Prediction · InferenceService<br/>DriftCalculator · AlertManager · RoutingStrategy<br/>CircuitBreaker · FeatureStore (ABC) · EventBus"]
    end
    subgraph Shared
        S["Exceptions · Interfaces · Utilities"]
    end
    Infrastructure --> Application --> Domain --> Shared

3.1 Dependency Rule

graph LR
    Infrastructure --> Application --> Domain --> Shared
  • Domain Layer does NOT import FastAPI, SQLAlchemy, Redis, Kafka, or ONNX Runtime
  • Application Layer only imports Domain entities/services
  • Infrastructure Layer implements Domain ABCs (interfaces)

3.2 Source Code Structure

phoenix_ml/
├── config/                         # 5 files — Settings (Pydantic, reads .env)
│   ├── app.py                      #   APP_VERSION, DEFAULT_MODEL_ID
│   ├── inference.py                #   BATCH_MAX_SIZE, INFERENCE_ENGINE
│   ├── infrastructure.py           #   DATABASE_URL, KAFKA_URL, REDIS_URL
│   └── monitoring.py               #   MONITORING_INTERVAL_SECONDS
│
├── domain/                         # 40+ files — Pure business logic
│   ├── inference/                  #   ML prediction bounded context
│   │   ├── entities/               #     Model, Prediction
│   │   ├── value_objects/          #     ConfidenceScore, FeatureVector, LatencyBudget
│   │   ├── services/               #     InferenceEngine, InferenceService, BatchManager
│   │   │                           #     RoutingStrategy, CircuitBreaker, RequestPipeline
│   │   └── events/                 #     ModelLoaded, PredictionMade
│   ├── monitoring/                 #   Drift & alerting bounded context
│   │   ├── entities/               #     DriftReport
│   │   ├── services/               #     DriftCalculator, AlertManager, AnomalyDetector
│   │   │                           #     MetricsPublisher, ModelEvaluator, RollbackManager
│   │   └── repositories/          #     DriftReportRepository, PredictionLogRepository
│   ├── training/                   #   Training bounded context
│   │   ├── entities/               #     TrainingConfig, TrainingJob
│   │   ├── services/               #     TrainingService, IDataLoader, ITrainer
│   │   └── events/                 #     TrainingCompleted
│   ├── feature_store/              #   Feature management bounded context
│   │   ├── entities/               #     FeatureRegistry
│   │   └── repositories/          #     FeatureStore, OfflineFeatureStore
│   ├── model_registry/            #   Model versioning bounded context
│   │   └── repositories/          #     ModelRepository, ArtifactStorage
│   └── shared/                     #   Shared kernel
│       ├── domain_events.py        #     PredictionCompleted, DriftDetected, ModelRetrained
│       ├── event_bus.py            #     DomainEventBus (Observer pattern)
│       └── plugin_registry.py     #     PluginRegistry (pre/post processors)
│
├── application/                    # 14 files — Use cases (CQRS)
│   ├── commands/                   #   PredictCommand, BatchPredictCommand, LoadModelCommand
│   ├── handlers/                   #   PredictHandler, BatchPredictHandler, RetrainHandler
│   │                               #   QueryHandlers (GetModel, GetDrift, GetPerformance)
│   ├── services/                   #   MonitoringService
│   ├── dto/                        #   PredictionRequest, PredictionResponse
│   └── decorators.py              #   @timing, @retry decorators
│
├── infrastructure/                 # 37 files — Framework adapters
│   ├── bootstrap/                  #   container.py (DI), lifespan.py (startup/shutdown)
│   ├── http/                       #   FastAPI routes, dependencies
│   ├── grpc/                       #   gRPC server + proto definitions
│   ├── ml_engines/                 #   ONNX, TensorRT, Triton, Mock
│   ├── messaging/                  #   Kafka producer + consumer
│   ├── feature_store/              #   Redis, InMemory, Parquet
│   ├── persistence/                #   PostgreSQL repos, MLflow, InMemory
│   ├── monitoring/                 #   Prometheus, Jaeger, AlertNotifier
│   ├── artifact_storage/          #   Local, S3/MinIO
│   └── data_loaders/             #   Tabular (CSV), Image (NPZ/dir)
│
└── shared/                         # 9 files — Cross-cutting concerns
    ├── exceptions/                 #   ModelNotFoundError, InferenceError, etc.
    ├── ingestion/                  #   API/Redis data ingestors, DataCollector
    └── utils/                      #   model_generator (ONNX for CI)

4. Design Patterns

4.1 Pattern Catalog

Pattern Location Purpose Details
Strategy RoutingStrategy Model traffic routing 4 strategies: SingleModel (100% champion), ABTest (split by ratio), Canary (small % to challenger), Shadow (mirror, only return champion)
Circuit Breaker CircuitBreaker Fault tolerance 3 states: CLOSED → OPEN (when error rate > threshold) → HALF_OPEN (retry after timeout). Self-recovering
Chain of Responsibility RequestPipeline Request processing Pipeline steps: Validation → Cache Check → Feature Enrichment → Inference → Logging
Command/CQRS commands/, handlers/ Read/write separation Commands: PredictCommand, BatchPredictCommand. Queries: GetModelQuery, GetDriftReportQuery
Repository ModelRepository, FeatureStore Data access abstraction ABCs in domain, implementations in infrastructure
Observer DomainEventBus Event-driven architecture Subscribers auto-react: PredictionCompleted → publish Prometheus metrics, DriftDetected → trigger alert
Adapter Infrastructure layer Framework decoupling RedisFeatureStore adapts Redis → FeatureStore ABC, ONNXInferenceEngine adapts onnxruntime → InferenceEngine ABC
Factory container.py engine factory Engine selection Dict-based factory: {"onnx": ..., "tensorrt": ..., "triton": ...} — OCP: adding a new engine = adding one entry
Plugin PluginRegistry Model-specific processing Register preprocessor/postprocessor per model_id — extensible without modifying core
Singleton get_settings(), container.py Shared instances Module-level singletons for settings and DI container objects

4.2 Strategy Pattern — Routing

classDiagram
    class RoutingStrategy {
        <<abstract>>
        +select(champion, challengers) Model
    }
    class SingleModelStrategy {
        +select() → always champion
    }
    class ABTestStrategy {
        -traffic_ratio: float
        +select() → random split
    }
    class CanaryStrategy {
        -canary_percentage: float
        +select() → % small to challenger
    }
    class ShadowStrategy {
        +select() → mirror to both
    }

    RoutingStrategy <|-- SingleModelStrategy
    RoutingStrategy <|-- ABTestStrategy
    RoutingStrategy <|-- CanaryStrategy
    RoutingStrategy <|-- ShadowStrategy

4.3 Circuit Breaker — State Machine

stateDiagram-v2
    [*] --> CLOSED
    CLOSED --> OPEN: failure_count > threshold
    OPEN --> HALF_OPEN: after timeout period
    HALF_OPEN --> CLOSED: success
    HALF_OPEN --> OPEN: failure
    CLOSED --> CLOSED: success (reset counter)

5. Self-Healing Flow

5.1 Detection → Response Pipeline

sequenceDiagram
    participant MLoop as Monitoring Loop (Background)
    participant DC as DriftCalculator
    participant AD as AnomalyDetector
    participant AM as AlertManager
    participant AN as AlertNotifier (Slack)
    participant RM as RollbackManager
    participant EB as DomainEventBus
    participant AF as Airflow DAG

    MLoop->>DC: calculate_drift(reference, current)
    DC-->>MLoop: DriftReport(score=0.45, is_drifted=true)

    MLoop->>AD: detect_anomaly(recent_latencies)
    AD-->>MLoop: anomaly_indices=[42, 67, 89]

    MLoop->>AM: evaluate(rules, drift_score=0.45)

    alt score > 0.3 (CRITICAL threshold)
        AM->>AN: notify(Alert: "high_drift_score", severity=CRITICAL)
        AN->>AN: POST webhook → Slack
        AM->>RM: evaluate_rollback(model_id)
        RM->>RM: archive challengers, keep champion
    else score > 0.1 (WARNING threshold)
        AM->>AN: notify(Alert: "moderate_drift", severity=WARNING)
    end

    MLoop->>EB: publish(DriftDetected)
    EB->>AF: trigger retrain_pipeline DAG

5.2 Multi-Model Monitoring

The monitoring loop runs for all models configured in model_configs/:

  • Each model has its own reference data (models/<id>/v1/reference_data.json)
  • Each model has its own drift test type (ks, psi, chi2 — from YAML config)
  • Monitoring interval is configured via MONITORING_INTERVAL_SECONDS (default: 30 s in production)

6. Infrastructure Services

6.1 Docker Compose Stack

Service Image Port(s) Purpose
phoenix-api Custom (Dockerfile) 8000→8001 FastAPI API server + gRPC :50051
phoenix-frontend Custom (Dockerfile.frontend) 5173→5174 React dashboard (Vite dev server)
postgres postgres:15-alpine 5432→5433 Primary database (models, logs, reports)
redis redis:7-alpine 6379→6380 Online feature store + cache
kafka apache/kafka:latest 9092→9094 Event streaming (KRaft mode)
zookeeper — (built into KRaft) — Integrated into Kafka
kafka-ui provectuslabs/kafka-ui 8080→8082 Kafka cluster management UI
mlflow ghcr.io/mlflow/mlflow 5000→5001 Experiment tracking + model registry
prometheus prom/prometheus 9090→9091 Metrics scraping
grafana grafana/grafana 3000→3001 Dashboards (auto-provisioned)
jaeger jaegertracing/all-in-one 16686 Distributed tracing
minio minio/minio 9000/9001 S3-compatible artifact storage

6.2 Airflow Stack (docker-compose.airflow.yaml)

Service Port Purpose
airflow-webserver 8080 Airflow UI
airflow-scheduler — DAG scheduling
airflow-init — Initialize DB + create admin user
postgres-airflow 5432 Airflow metadata database

7. Data Flow

7.1 Prediction Data Flow

graph TD
    Client["Client Request"] --> API["FastAPI Router (routes.py)"]
    API --> Handler["PredictHandler (application/handlers/predict_handler.py)"]

    Handler --> Features{"Feature Retrieval"}
    Features -->|entity_id provided| FS["FeatureStore.get(id)"]
    Features -->|raw features| Inference["InferenceService"]
    FS --> Inference

    Inference --> Router["RoutingStrategy → select model"]
    Router --> CB{"CircuitBreaker"}
    CB -->|Proceed| Engine["InferenceEngine.predict(model, features)"]
    CB -->|OPEN| Error["Return 503 Error"]
    Engine --> Prediction["Prediction Result"]

    Prediction --> BG["Background Tasks (async)"]
    BG -.-> PG["Log to PostgreSQL"]
    BG -.-> Kafka["Publish to Kafka 'inference-events'"]
    BG -.-> Prom["Publish Prometheus metrics"]
    BG -.-> EB["Update EventBus subscribers"]

    Prediction --> Return["Return PredictionResponse to Client"]

7.2 Training Data Flow

graph TD
    DAG["Airflow DAG (dags/retrain_pipeline.py)"] --> Gen["generate_datasets.py"]
    Gen -->|"data/{model}/dataset.csv or .npz"| Train["examples/{model}/train.py"]

    Train --> Loader["DataLoader"]
    Loader --> Fit["sklearn/xgboost model → ONNX export"]
    Fit --> Save["models/{model}/v1/model.onnx + metrics.json"]

    Save --> Flow["MLflow: log params, metrics, artifacts"]
    Save --> PG["PostgreSQL: register model version"]
    PG --> Promote["Promote to champion stage"]

    subgraph "Self-Healing Retrain"
        Drift["Drift detected"] --> Export["POST /data/export-training"]
        Export --> Query["Query prediction_logs WHERE ground_truth = true"]
        Query --> Merge["Merge: baseline + fresh dataset"]
        Merge --> ReTrain["examples/{model}/train.py --data combined.csv"]
        ReTrain --> Register["POST /models/register (challenger)"]
    end

8. Test Architecture

8.1 Test Pyramid

Test Type Files Description
E2E 1 file Full flow test across API, DB, and inference
Integration 11 files API routes and service orchestration
Unit 48 files Focused testing of Domain, App, Infrastructure layers

8.2 Coverage

Layer Files Tests Coverage
Domain (unit) 18 ~120 95%+
Application (unit) 8 ~60 90%+
Infrastructure (unit) 24 ~180 85%+
Integration 11 ~50 —
E2E 1 ~10 —
Frontend 16 104 —
Total 78 ~524 87% backend

8.3 Quality Gates (CI)

uv run ruff check .           # Lint: 0 errors
uv run ruff format --check .  # Format: 243 files consistent
uv run mypy phoenix_ml/               # Type check: 0 issues
uv run pytest tests/           # Tests: all pass, 87% coverage
npx tsc --noEmit               # Frontend type check: 0 errors
npx eslint phoenix_ml/                # Frontend lint: 0 errors
npx vitest run                 # Frontend tests: 104/104 pass

Document Status: v4.0 — Updated March 2026