Skip to main content
Glama
SINGLETON_PATTERN_EXPLANATION.md25.4 kB
# Understanding Singletons: From Pattern to Problem to Solution **Audience**: Product Managers, Business Stakeholders, Technical Leaders **Purpose**: Understand why singletons were used in DollhouseMCP, why they became a bottleneck, and what fixing them enables **Reading Time**: 15 minutes --- ## Executive Summary DollhouseMCP was built using the **singleton pattern** - a common software design where certain system components exist as single, shared instances. This made perfect sense for a single-user command-line tool but became a critical limitation when scaling to multi-user deployment. **The Bottom Line:** - **What singletons gave us**: Simple, fast development for single-user scenarios - **What singletons cost us**: Inability to support multiple concurrent users without data corruption - **What refactoring delivers**: True multi-user capability, enabling SaaS deployment and team features - **Business impact**: Opens new revenue streams, market opportunities, and competitive positioning --- ## Section 1: What Are Singletons? ### The Simple Explanation A **singleton** is a design pattern where only one instance of a particular object exists throughout your application's lifetime. No matter where in the code you access it, you're always talking to the same single instance. ### Real-World Analogy: The Town Square Clock Imagine a town with one central clock in the square. Everyone in town looks at the same clock to know what time it is. There aren't multiple clocks - there's just THE clock. **Benefits:** - Everyone sees the same time (consistency) - No confusion about which clock is "correct" - Easy to find - everyone knows where it is - Saves resources - no need to maintain multiple clocks **Hidden Assumption:** - This works great as long as you only have one town When you need to serve multiple towns (multi-user scenarios), that single shared clock becomes a problem. Each town needs its own local time zone, schedules, and coordination. ### Code Example from DollhouseMCP Let's look at how `PortfolioManager` uses the singleton pattern: ```typescript export class PortfolioManager { private static instance: PortfolioManager; // Private constructor prevents creating new instances private constructor(config?: PortfolioConfig) { // Initialize the portfolio directory, element managers, etc. this.baseDir = path.join(homedir(), '.dollhouse', 'portfolio'); } // Everyone gets the same instance public static getInstance(): PortfolioManager { if (!PortfolioManager.instance) { PortfolioManager.instance = new PortfolioManager(); } return PortfolioManager.instance; } } ``` **What this means in plain English:** 1. The first time someone asks for the PortfolioManager, it gets created 2. Every subsequent request returns that same first instance 3. It's impossible to create a second PortfolioManager 4. All parts of the application share the same portfolio state --- ## Section 2: Why Singletons Were Used in DollhouseMCP ### The Original Use Case: Single-User stdio Transport DollhouseMCP initially communicated via **stdio** (standard input/output) - a simple pipe that connects one process to another. Think of it like a dedicated phone line between two people. ``` ┌──────────────┐ stdio pipe ┌──────────────┐ │ │◄──────────────────────────►│ │ │ Claude Code │ (one conversation) │ DollhouseMCP│ │ (client) │ │ (server) │ └──────────────┘ └──────────────┘ Single user state: • One active persona • One portfolio • One config ``` ### Why Singletons Made Perfect Sense In this environment, singletons were **the right choice**: 1. **Single User Guarantee**: Only one user could possibly be connected at a time 2. **Simple State Management**: One user = one state = one instance 3. **Performance**: No overhead of creating multiple instances 4. **Convenience**: Access managers from anywhere without passing dependencies 5. **Resource Efficiency**: Single file handles, single database connections ### The Embedded Assumptions The singleton architecture baked in several critical assumptions: | Assumption | Reality at Design Time | Reality for Multi-User | |------------|----------------------|----------------------| | "Only one user will ever be active" | ✅ True (stdio = 1:1) | ❌ False (SSE/WebSocket = 1:many) | | "State can be shared globally" | ✅ Safe (single context) | ❌ Dangerous (state leakage) | | "Configuration is system-wide" | ✅ Makes sense | ❌ Users need different configs | | "Active persona is a singleton" | ✅ One user, one persona | ❌ Each user needs their own | These weren't mistakes - they were **rational choices given the constraints at the time**. --- ## Section 3: The Problem with Singletons for Multi-User ### What Changes with Multi-User Architecture Modern MCP deployment uses **Server-Sent Events (SSE)** or **WebSocket** transports, which allow multiple simultaneous connections: ``` ┌──────────────┐ ┌──────────────┐ │ User A │───┐ │ │ │ (Claude Code)│ │ │ │ └──────────────┘ │ SSE/WebSocket │ │ ├─────────────────────────►│ DollhouseMCP│ ┌──────────────┐ │ (many connections) │ (server) │ │ User B │───┤ │ │ │ (Web Client) │ │ │ │ └──────────────┘ │ └──────────────┘ │ ┌──────────────┐ │ PROBLEM: │ User C │───┘ Still has single │ (API Key) │ shared instances! └──────────────┘ ``` ### Concrete Example: State Collision Let's walk through what happens with singleton managers when two users are active: **Timeline of Disaster:** ``` T=0: Server starts - PortfolioManager singleton created - ConfigManager singleton created - activePersona = null T=1: User A connects and activates "Creative Writer" persona - activePersona = "creative-writer" - State stored in singleton PortfolioManager T=2: User B connects and activates "Code Reviewer" persona - activePersona = "code-reviewer" ← OVERWRITES User A's setting! - State stored in SAME singleton PortfolioManager T=3: User A asks for writing help - Receives response in "Code Reviewer" voice ❌ - User A is confused - "I activated Creative Writer!" T=4: User A deactivates persona - activePersona = null ← AFFECTS User B too! - User B suddenly loses their Code Reviewer persona ❌ T=5: User C connects and loads portfolio - Triggers file system operations on shared PortfolioManager - May interrupt User A and User B's operations ❌ ``` ### Types of Failures #### 1. State Leakage **What it is**: One user's data bleeding into another user's session **Business impact**: - Privacy violations (User A sees User B's data) - Compliance failures (GDPR, data isolation requirements) - User trust erosion ("This app is showing me someone else's stuff") **Real scenario**: User A submits a persona to their GitHub account, but the submission goes to User B's account because they logged in while the request was processing. #### 2. Race Conditions **What it is**: Operations from different users interfere with each other **Business impact**: - Unpredictable behavior ("It works sometimes, breaks others") - Debugging nightmares (issues appear/disappear randomly) - Support costs skyrocket (hard to reproduce issues) **Real scenario**: User A and User B both try to reload the portfolio index simultaneously. The shared singleton tries to rebuild the same index twice, resulting in corrupted data structures or locked files. #### 3. Data Corruption **What it is**: Concurrent operations overwrite each other's work **Business impact**: - Lost work (user's changes disappear) - Data integrity failures (invalid states) - Recovery costs (support time, data restoration) **Real scenario**: User A creates a new memory element while User B is editing an existing one. Both operations write to the shared portfolio state, and User B's changes get overwritten with User A's partially complete data. #### 4. Configuration Conflicts **What it is**: Users can't have different settings **Business impact**: - Feature limitations (can't offer per-user customization) - Team friction (enterprise teams need different configs) - Market restrictions (can't sell team/organization features) **Real scenario**: User A (on free tier) and User B (on enterprise tier) should have different rate limits and feature access, but they share the same ConfigManager singleton, so both get the same settings. ### Why This Is a Show-Stopper for Growth From a business perspective, singletons create a **hard ceiling** on product evolution: | Business Goal | Blocked By Singletons | |--------------|----------------------| | SaaS Offering | ❌ Can't support multiple tenants safely | | Team Features | ❌ Can't isolate team configurations | | API Access | ❌ Can't handle concurrent API requests | | Enterprise Sales | ❌ Can't meet isolation/security requirements | | Scalability | ❌ Single bottleneck limits throughput | | White-Label | ❌ Can't customize per-customer | **In other words**: Singletons made DollhouseMCP fast to build but impossible to scale. --- ## Section 4: What Fixing Singletons Accomplishes ### The Refactored Approach: Dependency Injection Instead of global singletons, we create **isolated instances per user context**: **Before (Singleton):** ```typescript // Anywhere in the code: const portfolioManager = PortfolioManager.getInstance(); // Everyone gets the same instance ❌ ``` **After (Dependency Injection):** ```typescript // Each user connection creates its own context: class UserContext { readonly portfolioManager: PortfolioManager; readonly configManager: ConfigManager; readonly activePersona: string | null; constructor(userId: string) { // NEW instance for THIS user this.portfolioManager = new PortfolioManager({ userId }); this.configManager = new ConfigManager({ userId }); this.activePersona = null; } } // When User A connects: const userAContext = new UserContext('user-a'); // When User B connects: const userBContext = new UserContext('user-b'); // Completely isolated! ✅ ``` ### Visual: Before and After Architecture **Before (Singleton Architecture):** ``` User A ─┐ ├──► Single PortfolioManager ◄── All users share state User B ─┤ (global instance) (state collisions!) │ User C ─┘ Single ConfigManager Single MemoryManager Single ActivePersona ``` **After (Per-User Instances):** ``` User A ──► UserContext A ──► PortfolioManager A ──► ConfigManager A ──► MemoryManager A ──► ActivePersona A User B ──► UserContext B ──► PortfolioManager B ──► ConfigManager B ──► MemoryManager B ──► ActivePersona B User C ──► UserContext C ──► PortfolioManager C ──► ConfigManager C ──► MemoryManager C ──► ActivePersona C Each user has their own isolated state! ✅ ``` ### What This Enables: Business Capabilities Unlocked | Capability | How It Works | Business Value | |-----------|-------------|----------------| | **SaaS Deployment** | Multiple tenants on shared infrastructure | Recurring revenue model | | **Team Workspaces** | Shared portfolios with individual contexts | Team/organization pricing tier | | **API Access** | Handle concurrent API requests safely | Developer platform revenue | | **Usage-Based Billing** | Track per-user metrics accurately | Flexible pricing strategies | | **Enterprise Features** | Tenant-specific configs and security | Enterprise sales opportunities | | **White-Label** | Customer-specific branding and settings | Partnership and reseller channels | | **Audit & Compliance** | Per-user activity tracking | Meet enterprise security requirements | | **Horizontal Scaling** | Multiple server instances | Support unlimited users | ### Performance and Reliability Improvements **Before:** - Single bottleneck for all operations - Lock contention between users - Failure cascades (one user's crash affects all) - Unpredictable response times **After:** - Parallel processing per user - No cross-user interference - Failure isolation (one user's crash doesn't affect others) - Predictable, consistent performance ### The Cost-Benefit Analysis **Investment Required:** - Refactor singleton managers to accept user context - Update tool handlers to create/manage user contexts - Add user identification and session management - Test multi-user scenarios thoroughly **Time to Implement**: 4-6 weeks of focused development **Return on Investment**: - **Immediate**: Enables SSE/WebSocket deployment - **3 months**: SaaS beta with first paying customers - **6 months**: Team features for 2-3x ARPU (Average Revenue Per User) - **12 months**: Enterprise contracts with 10-100x ARPU **Risk of NOT doing it**: - Product stuck at single-user CLI forever - Competition moves to multi-user while we can't - Technical debt compounds (harder to fix later) - Market opportunity window closes --- ## Section 5: The Refactor in Progress ### The Big Picture: index.ts Modularization The main entry point (`src/index.ts`) is being refactored from **6,135 lines** down to ~**600 lines**. This isn't just about line count - it's about **separation of concerns**. **What 6,000 lines tells us:** - Everything is tangled together - Hard to test individual components - Difficult to understand what depends on what - Changes in one area break unexpected things elsewhere **What 600 lines enables:** - Clear boundaries between components - Easy to test in isolation - Obvious dependency relationships - Changes are localized and safe ### The Relationship Between Refactors Removing singletons and modularizing index.ts are **two sides of the same coin**: ``` ┌─────────────────────────────────────────────────────────────┐ │ OLD ARCHITECTURE │ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ index.ts (6,135 lines) │ │ │ │ │ │ │ │ • Direct calls to singleton.getInstance() │ │ │ │ • Global state management │ │ │ │ • Mixed concerns (transport, tools, business) │ │ │ │ • Hard-coded dependencies │ │ │ └───────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────┐ │ │ │ Singleton Managers │ │ │ │ (shared by all operations) │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ``` ┌─────────────────────────────────────────────────────────────┐ │ NEW ARCHITECTURE │ │ │ │ ┌──────────────────────┐ │ │ │ index.ts (~600) │ Thin orchestration layer │ │ └──────────┬───────────┘ │ │ │ │ │ ├──► ConnectionHandler (SSE/WebSocket support) │ │ ├──► UserContextManager (per-user state) │ │ ├──► ToolRegistry (modular tool system) │ │ └──► ServerLifecycle (startup/shutdown) │ │ │ │ Per-User Context: │ │ ┌──────────────────────────────────────────────────┐ │ │ │ UserContext (for User A) │ │ │ │ ├─ PortfolioManager (instance A) │ │ │ │ ├─ ConfigManager (instance A) │ │ │ │ ├─ MemoryManager (instance A) │ │ │ │ └─ ActivePersona (User A's choice) │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ UserContext (for User B) │ │ │ │ ├─ PortfolioManager (instance B) │ │ │ │ ├─ ConfigManager (instance B) │ │ │ │ └─ ... (isolated from User A) │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### What Each Refactor Enables | Refactor | Technical Win | Business Win | |----------|--------------|--------------| | **Singleton Removal** | Per-user state isolation | Multi-user deployment | | **Modularization** | Testable, maintainable code | Faster feature development | | **Combined Effect** | Clean, scalable architecture | Competitive time-to-market | ### The Migration Path The refactor is being done **incrementally** to minimize risk: **Phase 1: Foundation (Current)** - Modify managers to accept context/config instead of using singletons - Create UserContextManager to coordinate per-user instances - Add backwards compatibility layer (old code still works) **Phase 2: Transport Layer** - Update index.ts to detect connection type (stdio vs SSE/WebSocket) - Route stdio connections through legacy path (singleton) - Route SSE/WebSocket through new path (per-user context) **Phase 3: Tool Migration** - Update each tool handler to use UserContext - Test with multi-user scenarios - Gradually deprecate singleton code paths **Phase 4: Cleanup** - Remove singleton getInstance() methods - Remove backwards compatibility layer - Update documentation **Phase 5: Optimization** - Connection pooling - Resource limits per user - Advanced isolation features --- ## Glossary: Technical Terms Explained **Singleton Pattern**: A design where only one instance of a class exists application-wide **Dependency Injection**: Providing objects with their dependencies rather than having them create/fetch dependencies themselves **State Leakage**: When one user's data or settings unintentionally affect another user **Race Condition**: When the outcome depends on the timing/sequence of events (unpredictable) **stdio Transport**: Communication via standard input/output (command-line pipes) **SSE (Server-Sent Events)**: One-way communication from server to multiple clients (push notifications) **WebSocket**: Two-way communication channel allowing real-time bidirectional data flow **Refactoring**: Restructuring code without changing its external behavior **Separation of Concerns**: Organizing code so each module handles one clear responsibility **Modularization**: Breaking large files/systems into smaller, focused components --- ## Key Takeaways ### For Product Managers 1. **Singletons were the right choice for v1** (single-user CLI) but became a constraint for v2+ (multi-user deployment) 2. **The refactor isn't technical vanity** - it's the prerequisite for SaaS, teams, APIs, and enterprise features 3. **Timeline is 4-6 weeks** for core refactor, with immediate benefits for stability and future velocity 4. **ROI is clear**: Investment unlocks entire categories of revenue opportunities ### For Business Stakeholders 1. **Current state**: DollhouseMCP can only serve one user at a time safely 2. **Refactored state**: DollhouseMCP can serve unlimited users with complete isolation 3. **Market impact**: Moves us from "CLI tool" to "platform" category 4. **Competitive positioning**: Enables features competitors will need months to build ### For Technical Leaders 1. **This is foundational work** - all future multi-user features depend on it 2. **The refactor reduces complexity** - easier to onboard engineers, faster to ship features 3. **Testing becomes possible** - isolated components can be unit/integration tested properly 4. **Operational reliability improves** - failure isolation, better observability --- ## Questions and Answers **Q: Can't we just limit it to one user and ship faster?** A: Yes, short-term. But that permanently caps market opportunity at single-user CLI, missing SaaS revenue entirely. **Q: How long until we can ship multi-user features?** A: Core refactor: 4-6 weeks. First SaaS features: 2-4 weeks after that. Total: ~8-10 weeks to beta. **Q: What's the risk if we don't do this?** A: Product remains a niche CLI tool. Competition builds multi-user platforms while we're stuck. Market opportunity window closes. **Q: Can we do this incrementally while shipping features?** A: Yes. The refactor is designed to work alongside existing code. We can ship non-multi-user features during the transition. **Q: What breaks during the migration?** A: Nothing. We maintain backwards compatibility until everything is migrated, then flip the switch. **Q: Is this like "paying technical debt"?** A: Partially, but it's more "removing a design constraint." It's debt in the sense that we're correcting an assumption that no longer holds, but it's also investment in new capabilities. --- ## Conclusion The singleton pattern served DollhouseMCP well in its initial single-user, CLI-focused incarnation. As the product evolves toward multi-user deployment, those same singletons become critical bottlenecks. Refactoring away from singletons isn't about making the code "prettier" - it's about **removing the technical ceiling that prevents business growth**. The investment of 4-6 weeks unlocks SaaS deployment, team features, API access, and enterprise contracts - capabilities worth orders of magnitude more than the development cost. Combined with the ongoing modularization of index.ts, this refactor transforms DollhouseMCP from a well-built single-user tool into a scalable, maintainable platform ready for the next phase of growth. **The pattern that helped us build v1 must evolve to enable v2. That evolution is this refactor.** --- *Document Version: 1.0* *Last Updated: October 20, 2025* *Author: Technical Bridge Builder persona* *Audience: Product Managers, Business Stakeholders, Technical Leaders*

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server