We’ve all been there: you start a new project with the best intentions—maybe a Clean Architecture or Onion Architecture approach. You dutifully separate your layers: domain
, application
, infrastructure
, interfaces
, and so on. You create ports, adapters, repositories, aggregates, entities… and before you know it, you’re 15 files deep just to implement the simplest feature, trying to remember what the difference is between a “service interface” and a “domain interface.” By the time you add business logic, the overhead is huge, and half your code is either duplicative or plain boilerplate.
Molecular Architecture is a different way to tackle Domain-Driven Design. It’s not about ignoring domains; quite the opposite. The idea is to cut the nonsense and put your attention exactly where it belongs: on meaningful “working pieces of software” that directly represent your domain concepts, without the detour of excessive layers, abstract repositories, or adapters everywhere.
If you’ve ever wished you could treat each domain feature like a mini-application—runnable on its own when needed, but also easy to assemble into a bigger whole—then you’re in the right place. Think of it like microservices or “islands” within a single codebase, except you’re not spinning up 50 containers and deploying them separately. You have one codebase, with multiple self-contained modules (“molecules”) that can be run independently or combined.
Core Philosophy: Working Pieces of Software
At the heart of Molecular Architecture is the concept of a Working Piece of Software. Each module (or “molecule”) should:
- Stand Alone – It contains everything it needs to run by itself: database schema, HTTP controllers, domain logic, user interface (if relevant), etc.
-
Solve One Domain Problem – Each module represents a coherent domain concept (e.g.,
TodoList
,Chat
,Auth
,Payments
, etc.). -
Export What It Needs – Each module can be executed on its own (think
node run TodoList
), but it can also provide a library-like interface for bigger modules to consume. - Hide What’s Irrelevant – Don’t leak internal details if other modules don’t need them. This also helps keep the domain boundaries clearer.
Why Bother?
Why do this instead of layering up everything with Clean Architecture? Because layering can lead to large, horizontally bloated folders where your actual business logic gets drowned out by repetitive code. You end up writing a domain model, an application service, a repository interface, a repository implementation, a data mapper, a controller, an adapter, a port… you get the idea.
Molecular Architecture says: Stop. If you have a concept called TodoList
, just make a module called TodoList
. It can have a file or two (or a folder) where all domain logic, data handling, and external integrations for that concept live together. Then you decide if you want to spin it up alone or plug it into a larger app.
A Quick Comparison with Traditional Architectures
-
Clean/Onion Architecture
- Pros: Clear separation of concerns, well-defined layers, testability (via ports/adapters).
- Cons: Often leads to a lot of boilerplate, forcing you to create multiple classes/files for even trivial features. The lines between “infrastructure” and “domain” can become fuzzy (e.g., where do you put a simple array merge? Is that “technical” or “business”?).
-
Microservices
- Pros: Each service is autonomous; you can scale or deploy them separately.
- Cons: Infrastructure overhead, network boundaries, distributed system complexity. Overkill for many projects that don’t need that level of separation in production.
-
Molecular Architecture
- Pros: Minimal overhead, domain-focused, easy to remove or “kill” a feature by removing its folder, truly independent modules that can stand alone for testing or demonstration. It’s like microservices but all within a single process (unless you really want to break them out).
- Cons: You do need to carefully plan module boundaries and the dependency graph. Some code can become duplicated if you’re not disciplined about shared libraries. Testing might require a slightly different approach than the standard “mock everything” style.
Lego-Style Organization
Think of your system as a box of Lego bricks:
- Each molecule (brick) is a self-contained feature or domain concept (e.g.
Chat
,TodoList
). - Larger features (bigger bricks) can incorporate smaller bricks. For example, an
App
module might combineChat
andTodoList
. - The direction of dependencies is crucial: if
Auth
depends onTodoList
, thenTodoList
must not depend onAuth
. Keep it unidirectional to avoid circular references.
And the coolest part? If you decide you don’t want the Chat
feature anymore, just remove the Chat
folder. No leftover references scattered across your “infrastructure layer,” no big toggles or feature flags. Gone is gone.
Example Project Structure
Let’s illustrate a potential Node.js/TypeScript folder structure for a small system that has two main domain modules: TodoList and Chat. We’ll have a third “wrapper” module called App that orchestrates them.
my-cool-project/
├─ lib/
│ ├─ sql-service.ts // Shared library for SQL
│ └─ uuid.ts // Shared library for generating UUIDs
│
├─ modules/
│ ├─ todo-list/
│ │ ├─ todo-list.ts
│ │ ├─ task.ts
│ │ └─ main.ts // "Run" the TodoList module directly
│ │
│ ├─ chat/
│ │ ├─ chat.ts
│ │ ├─ conversation.ts
│ │ ├─ message.ts
│ │ └─ main.ts // "Run" the Chat module directly
│ │
│ └─ app/
│ └─ main.ts // This is the aggregator that runs everything
│
└─ package.json
Example: todo-list/main.ts
(Runnable Entry Point)
import { createServer } from 'http';
import { SqlService } from '../../lib/sql-service';
import { TodoList } from './todo-list';
async function main() {
// 1. Initialize your DB service (could be remote or in-memory)
const db = SqlService.make({ mode: 'remote' }); // or 'in-memory' for testing
// 2. Apply schema
await TodoList.init(db);
// 3. Create the HTTP server
const server = createServer((req, res) => {
// Suppose we have an HTTP controller that handles routes
TodoList.start(db, req, res);
});
// 4. Start listening
server.listen(3001, () => {
console.log('TodoList module is running on port 3001!');
});
}
In this setup:
-
TodoList
can run solo by doingnode modules/todo-list/main.js
. -
Chat
can run solo by doingnode modules/chat/main.js
. -
App
runs them both, orchestrating how they might interrelate.
That’s the gist: each module has a main.ts
that literally runs it. In bigger systems, you might have advanced ways to integrate them—like an API gateway or a shared UI. But the principle remains: each domain concept is encapsulated in its own place, with everything it needs.
Testing Strategy: No (or Minimal) Mocks
One of the common arguments for layered architectures is that they make tests simpler by letting you “inject” different infrastructure adapters. Molecular Architecture handles this differently:
-
In-Memory vs Remote
If you have a
SqlService
, it can be configured at startup to either connect to a real database or run in an in-memory mode (SQLite in-memory, for instance). That means no complicated mocking is required—just a different initialization parameter. -
Functional Testing
Because each module is fully runnable, you can spin it up and test it end-to-end. This can be more reliable than unit tests that mock half the universe.
-
Shared Services
For truly domain-agnostic utilities—like generating UUIDs, handling date/time, or logging—put them in a
lib/
folder. They don’t hold domain logic, so they’re easy to keep stable and test in isolation.
Example: Setting up a Test DB Mode
// lib/sql-service.ts
export function SqlService(mode: 'in-memory' | 'remote') {
if (options.mode === 'in-memory') {
// Return an in-memory DB instance, e.g. using SQLite
} else {
// Remote mode
}
}
In your test, you’d do:
import { SqlService } from '../lib/SqlService';
test('TodoList creation works', async () => {
const db = SqlService('in-memory');
// Then call your domain logic, e.g. create a new todo list
// Confirm results with in-memory checks
});
No MockSQLService
, no separate adapter. Just a single service that can switch modes.
Handling the Dependency Graph
One challenge (and also a key benefit) of Molecular Architecture is that you end up with a more explicit dependency graph. For instance:
-
Auth
depends onUser
. -
TodoList
depends onAuth
(maybe you need to check if the user is logged in). -
Chat
depends onUser
as well. -
App
depends onTodoList
,Chat
, andAuth
.
You might even create small bridging modules if needed. For example, if User
and TodoList
need some special integration logic, you can create a tiny bridging module called user-todo-integration
that depends on both. The rest of the system can choose to integrate that bridging module or not.
Yes, it’s more “mental overhead.” But you’re forced to think about the domain relationships. That’s the entire selling point of Domain-Driven Design—understanding and modeling your domain clearly. If a certain relationship is ambiguous or cyclical, that’s a sign to revisit your domain boundaries.
Addressing Common Critiques
1. “Isn’t This Just Microservices?”
Not exactly. You’re not forced to spin each module up as a separate process or container. You can if you want, but the default idea here is a single deployment artifact. The microservices-like advantage is that each feature is so well-isolated that you could peel it off if you ever needed to. Or you can keep it all in one monolith that’s easy to deploy.
2. “What About Repos, Entities, Aggregates?”
If your domain truly needs them, you can absolutely have classes or files named “Entity” or “Aggregate.” But you don’t have to. You might find that a straightforward approach—like a class or function called TodoList
with methods like addTask
, removeTask
—is enough. The key difference is you’re not forced to make a repository interface, a repository implementation, a domain entity, a domain interface, etc., just to store a row in the database.
3. “Layers Are Good for Separation of Concerns!”
Layers can be good, but they can also be overkill, especially when they artificially break up code that’s conceptually about the same thing. In Molecular Architecture, you can still separate business logic from technical code—just do it within the same module. For instance, have a todolist-business.ts
file with business logic and a todolist-db.ts
file with database logic inside your todo-list
module. That’s a smaller-scale layering that doesn’t lead to a system-wide sprawl.
4. “Won’t We End Up Duplicating Code?”
Potentially, yes—if multiple modules implement the same feature logic. The remedy is to pull truly shared code into a library module that is domain-neutral. Or, if the code is domain-specific, decide which domain “owns” it and let other domains depend on it in a unidirectional fashion. The key is to keep it explicit.
The Beauty of Killing Features
One of the underrated superpowers of Molecular Architecture is how trivial it is to remove a feature. Let’s say you built a “Chat” feature for your product, but nobody uses it. In a layered monolith, that code might be peppered across domain/
, app/
, infra/
, db migrations
, who knows where else. In Molecular Architecture, you literally:
- Delete the
chat
module folder. - Remove its mention from
app/main.ts
(if it was aggregated). - Done.
No messing around with feature flags or complicated branching. If the rest of your domain needed “just the user’s name” for something in Chat, they should’ve gotten it from the User
module, not from Chat
. So your domain dependencies remain unaffected (or they break loudly if they were incorrectly reaching into Chat’s domain—good, now you can fix that).
Points of Weakness (And Why We Accept Them)
-
Complex Dependency Graph
Yes, figuring out which module depends on which can be more work up front. But that’s the domain modeling you’re supposed to be doing anyway. If it’s complicated, it might reflect real domain complexity.
-
Potential Duplication
If you don’t define clear boundaries or shared libraries, you might rewrite the same logic in multiple modules. The solution is to carefully factor out domain-agnostic code into a library. For domain-specific logic, decide which module truly owns it.
-
Testing Without Mocks
Some developers love mocking everything. This architecture tends to prefer real (but configurable) services—like an in-memory DB for tests. It’s a matter of taste, but we argue it leads to more reliable tests and less mock scaffolding.
-
Scalability
As your application grows, you must keep modules well-structured, watch for cyclical dependencies, and maintain clarity in how the domain is split. This is an ongoing effort, but you’d be doing something similar with bounding contexts in a traditional DDD approach anyway.
Final Thoughts
Molecular Architecture is about focusing on meaningful domain concepts and building fully functional chunks of software around them. It’s:
- Pragmatic: Instead of layering your domain into oblivion, you keep the code close together—domain logic, database schema, and interfaces that matter to that domain.
- Flexible: Each module can be developed, tested, and even run independently.
- Explicit: Dependencies are actual references between modules, not hidden behind an ocean of adapters and abstract interfaces. You have to think about them, which is the whole point.
- Removable: If a feature isn’t needed, kill it. Delete the folder, remove it from the aggregator. Done.
Yes, you’ll spend more time discussing and refining your module boundaries and dependency graph. But that’s domain-driven design in a nutshell—tackling the real complexity head-on, not burying it in layers. Meanwhile, you avoid the swirling black hole of 50-file sprawl for every little feature.
If you’re looking for a middle ground between microservices and monolithic layered architectures, give Molecular Architecture a shot. It lets you keep a single app in production, but develop your features as if they were independent micro-applications. It’s domain-driven design made practical, without forcing you to write novel-length code just to store a single row in a database.
Happy building—go forth and assemble your molecules!
Source link
lol