The Beauty of Clean Architecture: Swapping Core DI and Hashing Engines in 60 Minutes
WebAssembly (WASM) and browser-based runtimes like Pyodide are quietly rewriting the rules of application portability. Running python-based backends directly in the browser—with near-native speeds, zero server cost, and instant startup—is no longer a futuristic dream. It is here today.
But running enterprise-grade python frameworks in WASM comes with a major catch: C-extensions are a brick wall.
Recently, I decided to test the limits of Framework M's portability. My goal was simple: get our entire modular monolith booting directly inside the browser using Pyodide.
The result of that experiment taught me one of the most powerful, real-world lessons about the true beauty of clean, decoupled architecture.
💡 The Spark of Inspiration
The web ecosystem is collectively realizing that lightweight, highly portable core architectures are the future. Seeing the open discussions in the broader Python and enterprise web communities around the painful, heroic efforts required to make mature frameworks lighter and split heavy monoliths got me thinking about Framework M.
If we want absolute deployment sovereignty—running the exact same codebase on massive cloud clusters, local offline laptops, edge IoT devices, and directly inside a client's web browser—our core must be extremely agile and free from heavy native platform ties.
So, I fired up Pyodide and tried to boot Framework M.
Almost immediately, the runtime choked. The issue wasn't our business logic, our routing, or our database adapters. The blocker came from two popular, compiled C-dependencies deep in our core stack:
dependency-injector: A highly optimized DI library that relies on native C-extensions for performance.argon2-cffi: An incredibly secure hashing engine, but one that binds directly to native C libraries.
In standard server environments, these C-extensions compile and run flawlessly. In the sandboxed WebAssembly environment of the browser, they simply won't run without custom, complex WASM compilations.
We had to hot-swap our core DI container and password hashing engine, replacing these native compiled dependencies with pure-Python and standard-library implementations.
⚙️ The Fear of Hot-Swapping Core Engines
Let's be completely fair: swapping a password hashing algorithm is relatively trivial. In any codebase with decent design, your password hashing utility is already tucked neatly behind a standard authentication interface. Changing the algorithm is just a matter of changing a few lines of code in that specific adapter.
But hot-swapping the Dependency Injection engine itself? That is a completely different beast.
In most traditional framework architectures, the DI container is not an option—it is the very glue of the system. Its framework-specific decorators, annotations, and base classes leak into every single service, controller, utility, and database repository.
If you decide to change or replace your DI framework, you are looking at a terrifying proposition:
- Touching every single file across your codebase to rewrite import modules and DI decorator signatures.
- Facing a cascading waterfall of broken lifecycle hooks, container startup delays, and deep import cycles.
- Spending weeks in a risky, high-stress refactoring loop.
But with Framework M, I wanted to see if our strict adherence to the Ports and Adapters (Hexagonal) architecture would pay off under pressure.
⚡ The 60-Minute Swap
Because Framework M was designed from Day 1 with absolute decoupling, the core domain doesn't know how dependencies are injected, nor does it know the exact binary implementation of how a password is encrypted. It only knows about abstract protocols (Ports).
The concrete DI container and the hashing engine are simply Adapters plugged in at the edge of the application.
Here is exactly how the swap went:
- The DI Port: We defined our container and providers using clean Python protocols. To replace the C-extension
dependency-injector, we wrote a lightweight, pure-Python DI container that implements the exact same abstract interfaces. Because our business domains import only core interfaces rather than concrete container decorators, we didn't have to touch a single business logic file. - The Hashing Port: We completely eliminated
argon2-cffifrom our stack. We replaced it entirely by leveraging modern Python's robusthashlibstandard library—specificallyhashlib.pbkdf2_hmacusing PBKDF2-HMAC-SHA256 with 600,000 iterations—which runs natively and securely on any platform out of the box (including WebAssembly) without any extra packages, compile flags, or native C dependencies. - The Result: We swapped the DI container, updated our bootstrap bindings to use the new standard library hashing adapters, and ran the test suite.
It took exactly one hour.
There were no broken imports, no cascading refactors across business apps, and no manual untangling of global states. Every single unit, integration, and database test passed successfully on the very first run.
Today, Framework M boots in milliseconds inside a standard browser tab via Pyodide—fully functional, fully tested, and completely free of native platform shackles.
🎮 Live Interactive WASM Playground
Don't just take my word for it. You can boot Framework M directly inside your browser right now.
The console below streams a live WebAssembly environment. It dynamically fetches the pure-Python distribution of Framework M, automatically intercepts and mocks C-extension dependencies on-the-fly, boots our DI Container in a 100% database-free bootstrap mode, and executes standard business logic in a sandboxed WASM container.
🛠️ How It Works Under the Hood
Curious how we booted Framework M—directly inside a React component?
You don't need to guess—you can always refer to the source code! Here is the exact React hook, package loader, and Auto-Mocker setup that makes this in-browser sandboxed playground work:
import * as React from "react";
import { useState, useEffect } from "react";
import { loadPyodide } from "pyodide";
export function usePyodideApp() {
const [pyodide, setPyodide] = useState(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
async function initPyodide() {
// 1. Initialize Pyodide with the standard library 'ssl' package
// This enables full hashlib cryptography support natively.
const py = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/",
});
await py.loadPackage(["micropip", "ssl"]);
const micropip = py.pyimport("micropip");
// 2. The Auto-Mocker: Intercept and stub compiled C-extensions on-the-fly
async function installWithAutoMock(packages: string[]) {
let success = false;
while (!success) {
try {
await micropip.install(packages);
success = true;
} catch (err: any) {
// Intercept standard missing pure-Python wheel exceptions
const match = err.message.match(/Can't find a pure Python 3 wheel for '([a-zA-Z0-9_-]+)/);
if (match) {
const pkg = match[1];
console.warn(`[Auto-Mocker] Stubbing server C-extension dependency: ${pkg}`);
micropip.add_mock_package(pkg, "99.9.9");
} else {
throw err; // Re-throw actual network or parsing exceptions
}
}
}
}
// 3. Download the standard, pure-Python framework packages from PyPI!
await installWithAutoMock([
"pydantic",
"pydantic-settings",
"framework-m==0.11.1"
]);
// 4. Boot Framework M in 100% Stateless & Database-Free Mode
py.runPython(`
import os
os.environ["DATABASE_URL"] = "none"
os.environ["BOOT_MODE"] = "wasm"
from framework_m import Container
# Initialize the container DI
container = Container()
container.config.from_dict({"environment": "In-Browser WebAssembly"})
`);
setPyodide(py);
setIsReady(true);
}
initPyodide();
}, []);
}
🎨 The Real Value of Clean Architecture
Engineers often debate clean architecture, ports and adapters, and SOLID principles as if they are academic exercises or "over-engineering."
This experiment proved they are anything but.
Decoupling is an operational superpower. It ensures that your business logic remains completely timeless, independent of physical hardware, operating systems, or runtime limitations. When the next decade introduces a radical new runtime shift, a well-architected codebase won't need a year of painful rewrites.
It will just need a new adapter.
