ADR-0006: MX Pattern - Building Database Variants Without Forking
- Status: Accepted
- Date: 2026-01-27
- Deciders: @anshpansuriya14
- Supersedes: N/A
- Superseded by: N/A
Context
Enterprise customers often have non-negotiable database requirements:
- MongoDB-only shops: "We standardized on MongoDB. SQLAlchemy is not an option."
- Cassandra for scale: "We need distributed writes. PostgreSQL won't work."
- Legacy Oracle: "All our data is in Oracle. We can't migrate."
Traditional frameworks force you to:
- Fork the framework (maintenance nightmare)
- Accept their database (organizational conflict)
- Build from scratch (expensive)
Framework-M solves this with the MX Pattern: Pure package composition enables database customization without forking.
Problem Statement
The Enterprise Dilemma
A company using MongoDB wants Framework-M's features but:
- ❌ Can't use SQLAlchemy (organizational policy)
- ❌ Can't fork Framework-M (maintenance cost)
- ❌ Can't afford to build everything from scratch
Traditional Solution: Fork the framework, rip out SQLAlchemy, add MongoDB → Now you own a fork forever.
MX Pattern Solution: Install framework-mx-mongo package → MongoDB becomes the database without touching core code.
Decision
We split Framework-M into three layers to enable the MX pattern:
1. Core (Protocols Only)
Package: framework-m-core
Contains:
- ✅ All Protocols (Interfaces):
RepositoryProtocol,SchemaMapperProtocol,UnitOfWorkProtocol, etc. - ✅ DI Container (
dependency-injector) - ✅ BaseDocType system
- ✅ Controller/Hook framework
- ✅ CLI foundation (
cyclopts)
Does NOT contain:
- ❌ Any database adapters
- ❌ SQLAlchemy imports
- ❌ Concrete implementations
Philosophy: Core defines what the system needs, not how to provide it.
# framework-m-core: Pure protocol
from typing import Protocol
class RepositoryProtocol[T](Protocol):
async def get(self, id: UUID) -> T | None: ...
async def save(self, entity: T) -> T: ...
# No SQLAlchemy, no MongoDB - just the contract
2. Standard (SQLAlchemy Adapters)
Package: framework-m-standard
Contains:
- ✅ SQLAlchemy implementations of all protocols
- ✅ PostgreSQL/MySQL/SQLite support
- ✅ Litestar web framework integration
- ✅ NATS event bus adapter
- ✅ Redis cache adapter
Philosophy: The "default" batteries for 80% of users.
# framework-m-standard: SQLAlchemy implementation
from framework_m_core.interfaces.repository import RepositoryProtocol
class GenericRepository[T](RepositoryProtocol[T]):
async def get(self, id: UUID) -> T | None:
# SQLAlchemy implementation
result = await session.execute(select(...))
return result.scalar_one_or_none()
3. MX Packages (Custom Adapters)
Example: framework-mx-mongo
Contains:
- ✅ MongoDB implementations of all protocols
- ✅ Motor (async MongoDB driver) integration
- ✅ JSON Schema validators for Pydantic models
- ✅ MongoDB transaction support
Philosophy: Enterprises can publish their own framework-mx-* packages for custom databases.
# framework-mx-mongo: MongoDB implementation
from framework_m_core.interfaces.repository import RepositoryProtocol
from motor.motor_asyncio import AsyncIOMotorDatabase
class MongoGenericRepository[T](RepositoryProtocol[T]):
async def get(self, id: UUID) -> T | None:
# MongoDB implementation
doc = await self._collection.find_one({"id": str(id)})
return self._model.model_validate(doc) if doc else None
How It Works: Entry Points
The discovery mechanism uses Python's entry_points system:
1. Protocol Registration
Framework-M-Standard registers its adapters:
# libs/framework-m-standard/pyproject.toml
[project.entry-points."framework_m.adapters.repository"]
default = "framework_m_standard.adapters.db.generic_repository:GenericRepository"
[project.entry-points."framework_m.adapters.schema_mapper"]
default = "framework_m_standard.adapters.db.schema_mapper:SchemaMapper"
[project.entry-points."framework_m.adapters.unit_of_work"]
default = "framework_m_standard.adapters.db.session:SessionFactory"
2. MX Override
Framework-MX-Mongo registers the same entry point names, overriding the defaults:
# libs/framework-mx-mongo/pyproject.toml
[project.entry-points."framework_m.adapters.repository"]
default = "framework_mx_mongo.repository:MongoGenericRepository"
[project.entry-points."framework_m.adapters.schema_mapper"]
default = "framework_mx_mongo.schema_mapper:MongoSchemaMapper"
[project.entry-points."framework_m.adapters.unit_of_work"]
default = "framework_mx_mongo.unit_of_work:MongoSessionFactory"
3. Bootstrap Discovery
At startup, Framework-M:
- Scans all
framework_m.adapters.*entry points - Binds discovered adapters to DI container
- Priority: MX package > Standard > Core
# Automatic discovery at startup
from importlib.metadata import entry_points
def discover_adapters():
for ep in entry_points(group="framework_m.adapters.repository"):
if ep.name == "default":
adapter_class = ep.load()
container.bind(RepositoryProtocol, adapter_class)
# If framework-mx-mongo installed, MongoGenericRepository wins
Developer Experience
For Standard Users
# Install "batteries included"
pip install framework-m
# Uses SQLAlchemy by default
m migrate
m start
For MongoDB Users
# Install core + MongoDB variant
pip install framework-m-core framework-mx-mongo
# Now uses MongoDB everywhere
m migrate # Creates MongoDB collections
m start # Uses Motor for queries
Application Code (Identical!)
# This code works with BOTH SQLAlchemy and MongoDB!
from framework_m_core.interfaces.repository import RepositoryProtocol
class UserService:
def __init__(self, repo: RepositoryProtocol[User]):
self._repo = repo # Injected by DI
async def get_user(self, id: UUID) -> User | None:
return await self._repo.get(id)
# Works with SQL or MongoDB - protocol guarantees compatibility
Key Insight: Application code depends on Protocols, not concrete adapters. The DI container handles the binding.
Benefits
1. No Fork Required
✅ Enterprises customize by installing packages, not forking code
✅ Upstream updates flow automatically
✅ No merge conflicts, no drift
2. Zero Vendor Lock-in
✅ Switch from SQL → MongoDB → Cassandra by changing pip install
✅ Test with SQLite, deploy with PostgreSQL, migrate to MongoDB later
✅ Multi-database applications (SQL for transactions, MongoDB for logs)
3. Clean Separation
✅ Core remains infrastructure-agnostic
✅ Standard provides "good defaults"
✅ MX packages are independent, publishable artifacts
4. Ecosystem Growth
✅ Community can publish framework-mx-cassandra, framework-mx-dynamodb, etc.
✅ Framework-M doesn't need to support every database
✅ Best-of-breed adapters compete in the ecosystem
Trade-offs
Accepted Trade-offs
| Trade-off | Why Acceptable |
|---|---|
| Complexity: Three packages instead of one | 80% of users install framework-m meta-package and never think about it |
| Protocol Overhead: Defining interfaces requires discipline | Type safety and testability gains outweigh boilerplate |
| Discovery Latency: Entry point scanning at startup | Happens once per boot; negligible compared to DB connection |
Mitigations
- Meta-package:
framework-mbundles Core + Standard for simple installs - Documentation: Clear tutorials for both standard and MX paths
- Testing: Protocol compliance tests ensure adapters are compatible
Implementation Checklist
Core Package
- Extract all Protocols to
framework-m-core - Remove all SQLAlchemy imports from core
- Define entry point groups for adapters
- Implement DI auto-binding from entry points
Standard Package
- Move SQLAlchemy adapters to
framework-m-standard - Register entry points for all adapters
- Maintain 100% backward compatibility
MX Example
- Create
framework-mx-mongoas proof-of-concept - Implement all required protocols
- Demonstrate override mechanism works
- Write comprehensive tutorial
Documentation
- Protocol reference guide
- MX pattern architecture (this document)
- MongoDB MX tutorial
- Guide: "Building Custom MX Packages"
Protocol Inventory
Framework-M defines these core protocols:
| Protocol | Purpose | Entry Point Group |
|---|---|---|
RepositoryProtocol | CRUD operations | framework_m.adapters.repository |
SchemaMapperProtocol | Schema creation/migration | framework_m.adapters.schema_mapper |
UnitOfWorkProtocol | Transaction management | framework_m.adapters.unit_of_work |
BootstrapProtocol | Startup initialization | framework_m.bootstrap |
PermissionProtocol | Authorization | framework_m.adapters.permission |
EventBusProtocol | Pub/sub messaging | framework_m.adapters.event_bus |
CacheProtocol | Caching layer | framework_m.adapters.cache |
Each protocol is:
- ✅ Database-agnostic (no SQLAlchemy types in signatures)
- ✅ Async-first
- ✅ Type-safe (full generics support)
- ✅ Runtime-checkable
Real-World Example: MongoDB Primary
Scenario
Company: Large financial institution
Requirement: All data must be in MongoDB (compliance + expertise)
Challenge: Want Framework-M's metadata engine, RBAC, and DocType system
Solution
# Install MX variant instead of standard
pip install framework-m-core framework-mx-mongo
# Configure MongoDB connection
export DATABASE_URL="mongodb://localhost:27017/myapp"
# Bootstrap
m migrate # Creates MongoDB collections with JSON Schema validators
m start # Litestar + Motor + Framework-M
Application Code
# doctypes/invoice.py - IDENTICAL to SQL version!
from framework_m_core.domain import BaseDocType
from pydantic import Field
class Invoice(BaseDocType):
customer: str = Field(index=True)
amount: float
status: str = "Draft"
# controllers/invoice_controller.py
from framework_m_core.interfaces.repository import RepositoryProtocol
class InvoiceController:
def __init__(self, repo: RepositoryProtocol[Invoice]):
self._repo = repo
async def create_invoice(self, data: dict) -> Invoice:
invoice = Invoice.model_validate(data)
return await self._repo.save(invoice)
# Uses MongoDB via MongoGenericRepository - no code changes!
What Changed?
Code: Nothing
Dependencies: framework-m-standard → framework-mx-mongo
Database: PostgreSQL → MongoDB
Business Logic: Identical
Comparison: Framework-M vs Traditional Frameworks
| Aspect | Traditional Framework | Framework-M (MX Pattern) |
|---|---|---|
| Database Change | Fork + rewrite adapters | Install different package |
| Maintenance | Merge upstream forever | Auto-update via pip |
| Multi-DB Support | Hack job, fragile | Install both packages, configure binds |
| Testing | Hard to mock DB layer | Inject test doubles via protocols |
| Type Safety | Varies | 100% typed protocols |
| Lock-in | High (coupled to ORM) | None (protocol-based) |
Future Possibilities
MX Ecosystem
framework-mx-cassandra: Distributed writes, high availabilityframework-mx-dynamodb: AWS-native, serverlessframework-mx-neo4j: Graph database for complex relationshipsframework-mx-hybrid: SQL for transactions, MongoDB for events
Multi-Database Applications
# Use SQLAlchemy for transactional data
pip install framework-m-standard
# Use MongoDB for audit logs (write-heavy)
pip install framework-mx-mongo
# Configure binds in config
DATABASES = {
"default": "postgresql://...", # SQLAlchemy
"audit": "mongodb://...", # MongoDB
}
Application code uses the same RepositoryProtocol - DI binds the right adapter based on DocType metadata.
Alignment with Framework-M Principles
Ports & Adapters (Hexagonal Architecture)
✅ Ports: Protocols define the contracts (Core)
✅ Adapters: Implementations are pluggable (Standard/MX)
✅ Domain: Business logic depends only on protocols
No Vendor Lock-in
✅ Switching databases requires zero code changes
✅ Applications are portable across infrastructures
Type Safety
✅ Protocols enforce compile-time guarantees
✅ mypy --strict validates protocol compliance
Testability
✅ Mock repositories for unit tests
✅ In-memory adapters for integration tests
✅ Protocol compliance tests ensure compatibility
Conclusion
The MX Pattern enables enterprises to adopt Framework-M without database compromises:
- Core provides infrastructure-agnostic abstractions
- Standard offers battle-tested SQLAlchemy defaults
- MX packages customize for specific databases
Result: Organizations gain Framework-M's metadata engine, RBAC, and DocType system while using their required database - all without forking.
This architectural decision transforms Framework-M from "another opinionated framework" into a composable platform that respects enterprise constraints.
References
Appendix: Entry Point Groups
Framework-M defines these standard entry point groups:
Adapters
framework_m.adapters.repository
framework_m.adapters.schema_mapper
framework_m.adapters.unit_of_work
framework_m.adapters.permission
framework_m.adapters.event_bus
framework_m.adapters.cache
framework_m.adapters.storage
Bootstrap Steps
framework_m.bootstrap
CLI Commands
framework_m_core.cli_commands
Each group follows the convention:
- Name: Typically
"default"for primary adapter - Value:
"module.path:ClassName" - Priority: Last installed package wins (MX overrides Standard)
Status: This ADR is accepted and fully implemented in Phase 11.