Doh's core insight: all software operates through namespace resolution, and incompatible namespace systems can be made to work together through controlled composition rules.
Multiple inheritance is just one namespace incompatibility problem. The diamond problem revealed why naive combination fails—but also showed us the pattern for solving namespace conflicts in general.
The classic example that revealed why multiple inheritance is genuinely hard.
Animal
↙ ↘
Bird Horse
↘ ↙
Pegasus
move() → "flying"
move() → "galloping"
// Which one gets inherited?
pegasus.move() → ???
Just running both methods isn't enough. What's the return value? The execution order? How do they share state?
Ordering Should validation run before or after logging? Base before derived? Context matters.
Return Values One method returns altitude, another returns speed. Do you pick one? Average them? Create a tuple?
Hooking Need setup before methods run? Cleanup after? Simple stacking can't handle lifecycle hooks.
Language Chaos Every language invented different rules. C++ uses virtual inheritance. Python has MRO linearization. Scala has trait linearization.
The complexity exploded. Developers couldn't predict behavior. Debugging became a nightmare.
The Great Retreat Java banned it. C# followed. JavaScript never had it. Go rejected it from the start.
The mantra became universal: "Composition over inheritance."
The diamond problem has two core challenges:
Methods make this exponentially harder—functions can't just "merge" like objects or arrays can.
The diamond problem taught us the pattern for solving any namespace conflict: you need both ordering and melding rules. This same pattern applies beyond inheritance to all namespace incompatibilities.
Any namespace conflict needs ordering. We use topological sort to flatten complex hierarchies into predictable ordered lists.
Animal
↙ ↘
Bird Horse
↘ ↙
Pegasus
Resolves to: [Animal, Bird, Horse, Pegasus]
Four phases make this more powerful than this simple example shows: source identification, recursive expansion, deterministic flattening, and conditional evaluation. But the gist is: flatten the graph.
MOC is our type AND merge system in one—when multiple patterns define the same property, explicit rules determine how they combine.
Pattern('Vehicle', {
moc: {
move: 'Chain', // Chain methods together
speed: 'Method' // Blend returns onto this
},
move() { return 'base movement'; },
speed() { return { base: 10 }; }
});
Pattern('Bird', 'Vehicle', {
move(prev) { return prev + ' → flying'; },
speed() { return { air: 25 }; }
});
Pattern('Horse', 'Vehicle', {
move(prev) { return prev + ' → galloping'; },
speed() { return { ground: 35 }; }
});
Pattern('Pegasus', ['Bird', 'Horse'], {
move(prev) { return prev + ' → majestically'; }
});
const pegasus = New('Pegasus');
pegasus.move(); // "base movement → flying → galloping → majestically"
pegasus.speed; // { base: 10, air: 25, ground: 35 }
Results:
The same MOC rules work for arrays, objects, and complex melding scenarios.
MOC Rule Types (Some Commons):
'Method'
- Returns blend onto this
using MOC rules'Chain'
- Functions pipe return values to next function'Deep'
- Objects merge recursively'Array'
- Arrays merge uniquely, removing duplicatesMOC definitions themselves are inherited. Define behavior rules once, they cascade through your entire inheritance chain.
// Traditional Mixins
const LoggerMixin = {
log() { /* self-contained */ }
};
const ValidatorMixin = {
validate() { /* separate */ }
};
// Doh Patterns
Pattern('Base', {
moc: { process: 'Chain' }
});
// All descendants inherit the rule
Pattern('Child', 'Base', {
process(data) { /* adds to chain */ }
});
// Not "IS-A" or "HAS-A" but "DOES-ALL"
const component = New(['Draggable', 'Resizable', 'Clickable']);
// All behaviors are active and collaborative
// No conflicts, no ambiguity, no choosing sides
Maybe multiple inheritance can work?
We had a solution for multiple inheritance. But where were its limits? We kept looking for places where these core principles would break down.
Long before ESM existed, we had script tags executing immediately on parse. No dependency resolution, no guaranteed order. Testing our pattern system was impossible when you couldn't control execution flow.
Simple extension: jQuery's $(document).ready()
let you wait for DOM. Why not wait for other callbacks? Doh.Module(name, deps, callback)
was born—dependency-aware document.ready().
The realization: Modules are just another namespace system.
Each piece unlocked the next logical problem. As web standards matured, we leveraged what we could—JSON, Node.js, ESM. But the core insight remained: the same ordering and melding principles worked everywhere.
→ Auto-Packager emerges We wanted to inject args into our callbacks by name, and move modules around from file to file without updating anything. AST parsing scanned source code to understand what each module needed, built dependency graphs, generated manifests.
→ DohPath emerges
Moving modules around broke assets. We needed path resolution that worked regardless of where files lived. Context-relative paths (^/
) let you reference files relative to project root, module context, or packages.
→ Pod system emerges
As systems matured, modules needed configuration defaults. Not massive config files—sparse YAML that inherited and layered with deep melding and explicit removal operators (~~
).
→ Express middleware layer When Node.js came, Express was perfect—we just needed it to work with our module system. Thin compatibility layer for middleware integration, file-based routing, pod-driven configuration. No rebuilding for the sake of it.
→ SPR (Scoped Parameter Resolution) Functions need parameters, but declaring them at every call site breaks modularity. Parameters resolve by name through scope chains, enabling true reuse without redeclaration.
And this only scratches the surface. The same principles that solved the diamond problem became the foundation for security systems, data pipelines, development tools, cloud orchestration.
What started as solving the diamond problem became a framework for making any incompatible namespace systems work together. After years of applying ordering + melding principles across every domain we encountered created this surprisingly comprehensive ecosystem.
Load System & Auto-Packager: Universal dependency loading with build-time analysis
DohPath: Universal path resolution across environments
/
), context-relative (^/
), and package (^:pkg/
) pathsPatterns & MOC: Multiple inheritance object composition
Modules & SPR: Dependency injection with parameter resolution
Pods: Sparse configuration with deep melding
Express Router: Protocol-agnostic HTTP/WebSocket routing
Cloud Manager: Multi-instance orchestration
Data Pipeline: Cross-environment data processing
User System: Authentication and session management
Permission System: Context-aware authorization
Doh.permit(user, 'action:context', runtimeObject)
Browser IDE: Web-based development environment
MCP Server: AI assistant integration
The namespace composition principles have proven universal across domains:
Each system demonstrates the same pattern: when namespace systems are incompatible, ordering + melding rules provide the solution. The framework adapts to new namespace incompatibilities without breaking existing patterns, proving that this approach scales beyond any single problem domain.