✅ Accepted (Implemented)
The Exocortex codebase needs a scalable architecture that:
- Separates business logic from infrastructure concerns
- Enables testability through dependency injection
- Supports multiple interfaces (CLI, Obsidian plugin, potential Web/Mobile)
- Maintains long-term maintainability as the project grows
Early versions of the Obsidian plugin mixed business logic directly with Obsidian API calls:
// OLD: Tightly coupled code
export class TaskService {
constructor(private vault: Vault) {} // Obsidian-specific
async createTask(label: string): Promise<TFile> {
const uid = uuidv4();
const content = `---\nexo__Asset_uid: ${uid}\n---\n`;
return await this.vault.create(`${uid}.md`, content); // Direct Obsidian call
}
}This caused several problems:
- Untestable: Required full Obsidian environment for testing
- Not reusable: Business logic locked inside plugin
- Hard to maintain: Changes to Obsidian API required changes throughout codebase
- No CLI support: Could not automate tasks via command line
We adopt Clean Architecture with four distinct layers:
┌─────────────────────────────────────────────────┐
│ PRESENTATION (Obsidian Plugin) │
│ React components, Renderers, Modals, Views │
├─────────────────────────────────────────────────┤
│ APPLICATION (exocortex) │
│ CommandManager, Services, Use Cases │
├─────────────────────────────────────────────────┤
│ DOMAIN (exocortex) │
│ Entities, Constants, Business Rules │
├─────────────────────────────────────────────────┤
│ INFRASTRUCTURE (adapters) │
│ ObsidianVaultAdapter, NodeFsAdapter, etc. │
└─────────────────────────────────────────────────┘
Inner layers NEVER depend on outer layers.
- Domain knows nothing about Application, Infrastructure, or Presentation
- Application knows Domain but not Infrastructure or Presentation
- Infrastructure implements interfaces defined by Application
- Presentation uses Application services through DI
Monorepo Structure:
packages/
├── exocortex/ # Core (Domain + Application)
│ └── src/
│ ├── domain/ # Business entities, constants
│ ├── application/ # Use cases, services
│ └── infrastructure/ # Interfaces, pure utilities
├── obsidian-plugin/ # Presentation + Obsidian adapters
│ └── src/
│ ├── presentation/ # React components, UI
│ └── infrastructure/ # ObsidianVaultAdapter
└── cli/ # CLI Presentation + Node adapters
└── src/
├── executors/ # CLI commands
└── infrastructure/ # NodeFsAdapter
Dependency Injection with TSyringe:
// Domain Layer (no dependencies)
export interface IFileSystemAdapter {
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
createFile(path: string, content: string): Promise<void>;
}
// Application Layer (uses interface)
@injectable()
export class TaskCreationService {
constructor(
@inject("IFileSystemAdapter") private fs: IFileSystemAdapter
) {}
async createTask(label: string): Promise<string> {
const frontmatter = this.generateTaskFrontmatter(label); // Pure
const content = MetadataHelpers.buildFileContent(frontmatter); // Pure
await this.fs.createFile(path, content); // Via interface
return path;
}
}
// Infrastructure Layer (implements interface)
@injectable()
export class ObsidianVaultAdapter implements IFileSystemAdapter {
constructor(private vault: Vault) {}
async createFile(path: string, content: string): Promise<void> {
await this.vault.create(path, content);
}
}- Testability: Business logic tests require no Obsidian mocks
- Reusability: Core logic works in CLI, plugin, and future adapters
- Maintainability: Changes isolated to specific layers
- Parallel development: Teams can work on different adapters independently
- Framework agnostic: Business logic survives framework changes
- Clear boundaries: Easy to understand what belongs where
- More abstractions: Additional interfaces, adapters, DI containers
- Learning curve: Developers must understand Clean Architecture concepts
- Initial overhead: Setting up DI and monorepo structure takes time
- More files: Layer separation results in more source files
- Documentation: ARCHITECTURE.md explains layer responsibilities
- TSyringe: Lightweight DI (~2KB) with minimal boilerplate
- Templates: Standard patterns for new services documented in PATTERNS.md
- Code review: Enforce layer boundaries in PR reviews
Presentation → Business Logic → Data Access
Rejected because:
- Business logic layer can become coupled to data access
- Testing still requires mocking database/storage
- No clear separation for multiple adapters
Keep business logic mixed with framework code, refactor only when needed.
Rejected because:
- CLI requirement made refactoring inevitable
- Testing pain was already significant
- Future Web/Mobile adapters would be impossible
Very similar to Clean Architecture but with different terminology.
Why we chose Clean Architecture instead:
- Better documentation and community resources
- More explicit layer names (Domain, Application, Infrastructure)
- Same benefits as Hexagonal with clearer structure
- ADR-0006: Pure Functions Separation (predecessor)
- ADR-0009: Domain Separation Strategy (detailed domain boundaries)
- Issue #122: Core Extraction (implementation)
- Documentation: ARCHITECTURE.md
Date: 2026-02-20 Author: AI Development Team Related Issues: #122 (Core Extraction), #2188 (ADR Documentation)