Skip to main content

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:

  1. Fork the framework (maintenance nightmare)
  2. Accept their database (organizational conflict)
  3. 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:

  1. Scans all framework_m.adapters.* entry points
  2. Binds discovered adapters to DI container
  3. 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-offWhy Acceptable
Complexity: Three packages instead of one80% of users install framework-m meta-package and never think about it
Protocol Overhead: Defining interfaces requires disciplineType safety and testability gains outweigh boilerplate
Discovery Latency: Entry point scanning at startupHappens once per boot; negligible compared to DB connection

Mitigations

  • Meta-package: framework-m bundles 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-mongo as 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:

ProtocolPurposeEntry Point Group
RepositoryProtocolCRUD operationsframework_m.adapters.repository
SchemaMapperProtocolSchema creation/migrationframework_m.adapters.schema_mapper
UnitOfWorkProtocolTransaction managementframework_m.adapters.unit_of_work
BootstrapProtocolStartup initializationframework_m.bootstrap
PermissionProtocolAuthorizationframework_m.adapters.permission
EventBusProtocolPub/sub messagingframework_m.adapters.event_bus
CacheProtocolCaching layerframework_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-standardframework-mx-mongo
Database: PostgreSQL → MongoDB
Business Logic: Identical


Comparison: Framework-M vs Traditional Frameworks

AspectTraditional FrameworkFramework-M (MX Pattern)
Database ChangeFork + rewrite adaptersInstall different package
MaintenanceMerge upstream foreverAuto-update via pip
Multi-DB SupportHack job, fragileInstall both packages, configure binds
TestingHard to mock DB layerInject test doubles via protocols
Type SafetyVaries100% typed protocols
Lock-inHigh (coupled to ORM)None (protocol-based)

Future Possibilities

MX Ecosystem

  • framework-mx-cassandra: Distributed writes, high availability
  • framework-mx-dynamodb: AWS-native, serverless
  • framework-mx-neo4j: Graph database for complex relationships
  • framework-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:

  1. Core provides infrastructure-agnostic abstractions
  2. Standard offers battle-tested SQLAlchemy defaults
  3. 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.