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
- GitHub Repository: https://github.com/bvandewe/pyneuro
- Public Documentation: https://bvandewe.github.io/pyneuro/
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:
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
Related DocumentationΒΆ
- CQRS Pattern - Detailed CQRS implementation
- Data Layer - MongoDB integration patterns
- Dependency Injection - Service registration