Skip to main content

Framework M Fundamentals

Core concepts for understanding and extending Framework M.

[!IMPORTANT] Documentation Status: This guide shows planned patterns and API designs. Examples will be revisited in Phase 09B with real, working code including actual container initialization, DI wiring, and tested implementations.


0. Philosophy: Minimal Core, Maximum Flexibility

Only async Python and DI wiring are irreplaceable. Everything else can be replaced, extended, or composed.

The Irreplaceable Core

LayerWhatReplaceable?
LanguagePython 3.12+ async❌ No
DI ContainerContainer + wiring❌ No
ProtocolsInterface definitions❌ No (contracts)
EntrypointsPlugin discovery❌ No (mechanism)

Blessed Defaults (Deeply Integrated)

These are tightly coupled to the framework but have escape hatches:

ComponentDefaultEscape Hatch
HTTP FrameworkLitestarRoutes, middleware are Litestar-native (no abstraction)
ORMSQLAlchemyVirtual DocTypes with custom RepositoryProtocol implementations

Virtual DocTypes: For non-SQL data sources (APIs, NoSQL, files), implement RepositoryProtocol and register via RepositoryFactory.register_override():

# Custom repository for any data source
class MongoRepository(RepositoryProtocol[MyDoc]):
async def get(self, id: UUID) -> MyDoc | None: ...
async def save(self, entity: MyDoc) -> MyDoc: ...

# Register the override
RepositoryFactory().register_override("MyDoc", MongoRepository)

Everything Else is Replaceable

Components with Protocols can be swapped by implementing the interface and registering via entrypoints:

ComponentDefaultCan Swap ToProtocol
DatabasePostgreSQLMySQL, SQLite, Oracle(SQLAlchemy drivers)
Event BusNATSRedis, Kafka, RabbitMQEventBusProtocol
Job QueueTaskiqCelery, ARQJobQueueProtocol
CacheRedisMemcached, MemoryCacheProtocol
StorageS3Local, MemoryStorageProtocol
AuthLocal UserKeycloak, Auth0, customIdentityProtocol
FrontendRefine.devNext.js, Vue, Flutter(API-driven)

Open-Closed Principle

                    ┌─────────────────────────────┐
│ Your App / Custom Code │
└─────────────┬───────────────┘
│ uses
┌─────────────▼───────────────┐
│ Protocols │
│ (CacheProtocol, EventBus) │
└─────────────┬───────────────┘
│ implemented by
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Redis │ │ NATS │ │ Custom │
│ Adapter │ │ Adapter │ │ Adapter │
└───────────┘ └───────────┘ └───────────┘

Closed for modification: Protocols are stable contracts. Open for extension: Add new adapters via entrypoints.

Zero-Cliff Future

This design ensures:

  • No vendor lock-in — swap any component
  • No rewrite cliffs — extend, don't replace the whole framework
  • Progressive enhancement — start simple, add capabilities as needed
  • Test isolation — swap real adapters for mocks

1. Dependency Injection as Universal Composer

DI is not just for HTTP requests — it's the universal composition mechanism across:

ContextHow DI Works
Litestar (HTTP)Request-scoped via lifespan
CLI commandsApp-scoped via entrypoints
Background jobsJob-scoped via Taskiq
TestsOverrides via fixtures
Future (REPL, Notebooks)Same container pattern

1.1 The Container Pattern

# container.py - Central composition root
from framework_m.core.container import Container

def create_container(settings: Settings) -> Container:
"""Build the DI container with all dependencies."""
container = Container()

# Register adapters (implementations)
container.register(DatabaseAdapter, SQLAlchemyAdapter(settings.DATABASE_URL))
container.register(EventBus, NATSEventBus(settings.NATS_URL))
container.register(JobQueue, TaskiqQueue(settings.NATS_URL))
container.register(Cache, RedisCache(settings.REDIS_URL))

# Register repositories
container.register(ItemRepository, GenericRepository(Item, container.get(DatabaseAdapter)))

# Register services
container.register(ItemService, ItemService(container.get(ItemRepository)))

return container

1.2 Entrypoint Registration

Framework M discovers adapters via Python entrypoints (not hardcoded imports):

# pyproject.toml
[project.entry-points."framework_m.adapters.eventbus"]
nats = "framework_m.adapters.eventbus.nats:NATSEventBus"
redis = "framework_m.adapters.eventbus.redis:RedisEventBus"

[project.entry-points."framework_m.adapters.cache"]
redis = "framework_m.adapters.cache.redis:RedisCache"
memory = "framework_m.adapters.cache.memory:MemoryCache"

[project.entry-points."framework_m.adapters.storage"]
s3 = "framework_m.adapters.storage.s3:S3Storage"
local = "framework_m.adapters.storage.local:LocalStorage"

1.3 Configuration-Driven Selection

# settings.py
class Settings(BaseSettings):
# Select adapter by name (from entrypoints)
EVENT_BUS_ADAPTER: str = "nats"
CACHE_ADAPTER: str = "redis"
STORAGE_ADAPTER: str = "s3"

# bootstrap.py
def load_adapter(group: str, name: str):
"""Load adapter class from entrypoints."""
from importlib.metadata import entry_points
eps = entry_points(group=group)
return eps[name].load()

# Usage
EventBusClass = load_adapter("framework_m.adapters.eventbus", settings.EVENT_BUS_ADAPTER)
container.register(EventBus, EventBusClass(settings))

2. Bootstrap Flow

2.1 Litestar (HTTP Server)

# app.py
from litestar import Litestar
from litestar.di import Provide

def create_app() -> Litestar:
settings = Settings()
container = create_container(settings)

return Litestar(
route_handlers=[...],
dependencies={
"settings": Provide(lambda: settings),
"container": Provide(lambda: container),
"item_service": Provide(lambda: container.get(ItemService)),
},
lifespan=[container_lifespan],
)

async def container_lifespan(app: Litestar):
"""Startup/shutdown lifecycle."""
container = app.state.container
await container.startup() # Connect to DBs, queues
yield
await container.shutdown() # Cleanup

2.2 CLI Commands

# cli/commands.py
import cyclopts
from framework_m.core.bootstrap import create_container

app = cyclopts.App()

@app.command
def import_items(file: str):
"""CLI uses same DI container."""
settings = Settings()
container = create_container(settings)

# Get service from container
item_service = container.get(ItemService)

# Use it
items = parse_csv(file)
for item in items:
item_service.create(item)

2.3 Background Jobs (Taskiq)

# jobs/tasks.py
from taskiq import TaskiqDepends
from framework_m.core.bootstrap import get_container

@broker.task
async def process_order(
order_id: str,
order_service: OrderService = TaskiqDepends(lambda: get_container().get(OrderService)),
):
"""Job gets dependencies from same container."""
await order_service.process(order_id)

3. Repository + Controller + Service Composition

3.1 Layer Responsibilities

┌─────────────────────────────────────────────────────────────┐
│ HTTP / CLI / Job │
└─────────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ • Orchestrates business operations │
│ • Manages UnitOfWork (transactions) │
│ • Calls multiple repositories │
└─────────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ • DocType-specific business logic │
│ • Lifecycle hooks (validate, before_save, after_save) │
│ • Attached to Repository │
└─────────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Repository Layer │
│ • CRUD operations │
│ • Calls Controller hooks │
│ • Receives session, doesn't own it │
└─────────────────────────────┬───────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Database (SQLAlchemy) │
└─────────────────────────────────────────────────────────────┘

3.2 Composition Example

# 1. DocType (schema only)
class Invoice(BaseDocType):
customer: str
total: Decimal
items: list[InvoiceItem]

# 2. Controller (business logic + hooks)
class InvoiceController(BaseController[Invoice]):
async def validate(self, context=None):
if self.doc.total < 0:
raise ValueError("Total cannot be negative")

async def after_save(self, context=None):
await self.update_customer_balance()

# 3. Repository (CRUD + hooks invocation)
invoice_repo = GenericRepository(
model=Invoice,
table=invoice_table,
controller_class=InvoiceController, # Wired here
)

# 4. Service (orchestration + transactions)
class InvoiceService:
def __init__(self, repo: GenericRepository[Invoice], uow_factory: UoWFactory):
self.repo = repo
self.uow_factory = uow_factory

async def create_invoice(self, data: InvoiceCreate) -> Invoice:
async with self.uow_factory() as uow:
invoice = Invoice(**data.model_dump())
await self.repo.save(uow.session, invoice) # Calls controller hooks
await uow.commit()
return invoice

4. Adding Custom Adapters

4.1 Implement the Protocol

# my_app/adapters/custom_cache.py
from framework_m.core.interfaces import CacheProtocol

class MemcachedCache(CacheProtocol):
def __init__(self, url: str):
self.client = pymemcache.Client(url)

async def get(self, key: str) -> Any:
return self.client.get(key)

async def set(self, key: str, value: Any, ttl: int = 300):
self.client.set(key, value, expire=ttl)

4.2 Register via Entrypoint

# my_app/pyproject.toml
[project.entry-points."framework_m.adapters.cache"]
memcached = "my_app.adapters.custom_cache:MemcachedCache"

4.3 Select in Config

# .env
CACHE_ADAPTER=memcached
MEMCACHED_URL=memcached://localhost:11211

5. Testing with DI

5.1 Override Dependencies

# tests/conftest.py
import pytest
from framework_m.core.container import Container

@pytest.fixture
def test_container():
"""Container with test adapters."""
container = Container()
container.register(Cache, MemoryCache()) # In-memory for tests
container.register(EventBus, MockEventBus()) # Mock for tests
return container

@pytest.fixture
def item_service(test_container):
return test_container.get(ItemService)

5.2 Mock Specific Dependencies

@pytest.mark.asyncio
async def test_create_invoice(item_service, mocker):
# Mock specific method
mocker.patch.object(item_service.repo, "save", return_value=mock_invoice)

result = await item_service.create(data)
assert result.id == mock_invoice.id

Summary

ConceptKey Point
DI ContainerUniversal composer for all contexts
EntrypointsPluggable adapters without code changes
BootstrapSame container for HTTP, CLI, Jobs
CompositionService → Repository → Controller → DocType
TestingOverride adapters via container