Skip to content

System DesignΒΆ

This document describes the foundational architecture and framework patterns used across all Lablet Cloud Manager microservices.

Looking for Component Architecture?

For detailed architecture of each microservice, see the Architecture Overview and individual component pages.

Neuroglia-Python FrameworkΒΆ

All LCM microservices are built using the neuroglia-python framework, which promotes a clean, modular architecture based on Domain-Driven Design (DDD) and Command Query Responsibility Segregation (CQRS).

Framework Documentation

Clean Architecture LayersΒΆ

Each microservice follows the same layered structure:

graph TD
    subgraph "API Layer"
        API[Controllers, Dependencies, Middleware]
    end

    subgraph "Application Layer"
        App[Commands, Queries, Handlers, DTOs, Services]
    end

    subgraph "Domain Layer"
        Domain[Entities, Aggregates, Events, Repository Interfaces]
    end

    subgraph "Infrastructure Layer"
        Infra[Repository Implementations, API Clients, Adapters]
    end

    API --> App
    App --> Domain
    Infra --> Domain
    App --> Infra

Layer ResponsibilitiesΒΆ

Layer Directory Responsibility Dependencies
Domain domain/ Pure business logic, entities, invariants None (pure Python)
Application application/ Orchestration, CQRS handlers, services Domain
Infrastructure integration/ MongoDB, AWS, CML API clients Domain
API api/ HTTP controllers, auth, middleware Application

Key PrinciplesΒΆ

  • Dependency Rule: Inner layers know nothing about outer layers
  • Domain Isolation: Domain layer has no external dependencies
  • Repository Pattern: Abstract interfaces in domain, implementations in infrastructure
  • Mediator Pattern: Commands/Queries dispatched through centralized mediator

Directory Structure (Per Service)ΒΆ

service-name/
β”œβ”€β”€ api/                          # HTTP Layer
β”‚   β”œβ”€β”€ controllers/              # REST endpoints (Neuroglia auto-prefix)
β”‚   β”œβ”€β”€ dependencies.py           # FastAPI DI for auth, sessions
β”‚   └── services/                 # API-layer services (auth, SSE)
β”‚
β”œβ”€β”€ application/                  # Business Logic Layer
β”‚   β”œβ”€β”€ commands/                 # Write operations (self-contained)
β”‚   β”‚   └── create_entity_command.py  # Command + Handler in same file
β”‚   β”œβ”€β”€ queries/                  # Read operations (self-contained)
β”‚   β”‚   └── get_entities_query.py     # Query + Handler in same file
β”‚   β”œβ”€β”€ dtos/                     # Data Transfer Objects
β”‚   β”œβ”€β”€ services/                 # Application services
β”‚   β”œβ”€β”€ jobs/                     # Background tasks
β”‚   └── settings.py               # Configuration (Pydantic Settings)
β”‚
β”œβ”€β”€ domain/                       # Core Domain Layer
β”‚   β”œβ”€β”€ entities/                 # Aggregates and entities
β”‚   β”œβ”€β”€ events/                   # Domain events (@cloudevent)
β”‚   β”œβ”€β”€ enums/                    # Value objects and enums
β”‚   └── repositories/             # Abstract repository interfaces
β”‚
β”œβ”€β”€ integration/                  # External Service Adapters
β”‚   β”œβ”€β”€ repositories/             # Concrete repository implementations
β”‚   └── services/                 # External API clients
β”‚
β”œβ”€β”€ infrastructure/               # Technical Adapters
β”‚   └── session_store.py          # Redis/InMemory adapters
β”‚
β”œβ”€β”€ tests/                        # Test suite
β”œβ”€β”€ main.py                       # Application entrypoint
β”œβ”€β”€ Makefile                      # Development commands
└── pyproject.toml                # Dependencies

CQRS PatternΒΆ

Commands (writes) and Queries (reads) are handled separately through the Mediator:

# Self-contained command file: application/commands/create_worker_command.py

@dataclass
class CreateWorkerCommand(Command[OperationResult[WorkerCreatedDto]]):
    name: str
    region: str
    instance_type: str

class CreateWorkerCommandHandler(CommandHandler[CreateWorkerCommand, OperationResult[WorkerCreatedDto]]):
    def __init__(self, repository: CMLWorkerRepository):
        self._repository = repository

    async def handle_async(self, request: CreateWorkerCommand, cancellation_token=None):
        # Validation
        if not request.name:
            return self.bad_request("Name is required")

        # Business logic
        worker = CMLWorker.create(
            name=request.name,
            region=request.region,
            instance_type=request.instance_type
        )

        # Persistence
        await self._repository.add_async(worker, cancellation_token)

        # Response
        return self.created(WorkerCreatedDto(id=worker.id(), name=worker.state.name))

Controller RoutingΒΆ

Neuroglia auto-generates route prefixes from controller class names:

class WorkersController(ControllerBase):
    """Routes: /workers/*"""

    @route(HttpMethod.GET, "/")           # GET /workers/
    async def list_workers(self): ...

    @route(HttpMethod.GET, "/{id}")       # GET /workers/{id}
    async def get_worker(self, id: str): ...

    @route(HttpMethod.POST, "/")          # POST /workers/
    async def create_worker(self): ...

Avoid Double Prefixing

Do NOT include the prefix in route decorators:

# ❌ Wrong: GET /workers/workers/{id}
@route(HttpMethod.GET, "/workers/{id}")

# βœ… Correct: GET /workers/{id}
@route(HttpMethod.GET, "/{id}")

State-Based PersistenceΒΆ

Unlike the AIX platform which uses Event Sourcing, LCM uses State-Based Persistence:

class CMLWorker(AggregateRoot[CMLWorkerState]):
    """
    State-based aggregate with optimistic concurrency.

    - State stored directly in MongoDB
    - state_version field for conflict detection
    - No event stream (simpler model)
    """

Domain Events are still emitted as Cloudevents: See