Back to Blog
Technicalsaasai-developmentarchitectureabstractionlessons-learned

Building a Global SaaS: When AI Takes the MVP Path

A retrospective on building ContextFS with AI assistance and how AI tendency toward MVP solutions creates predictable category errors that require human architectural intervention.

Matthew LongJanuary 20, 20269 min read

When AI Takes the MVP Path

Why Abstraction Layers Matter in AI-Assisted Development


Key Metrics at a Glance

28+72 weeks → 2 days24x
Releases shippedCategory errors identifiedAbstraction tax reducedDebugging improvement

For Executives

AI coding assistants default to "make it work" solutions. Fast, but creates architectural debt. Most recurring issues traced to missing abstraction layers. The fix isn't less AI—it's knowing when to override AI's MVP instincts.

For Developers

AI writes working code that couples things that should be abstracted. SQLite everywhere? Works. Cached state? Works. Until it doesn't. This catalogs the category errors and abstractions AI skipped.


The Core Insight

When you ask an AI to implement something, it will give you working code.

The problem isn't that AI code doesn't work.

It's that AI systematically skips abstraction layers in favor of direct, coupled implementations.


What Is a Category Error?

A category error occurs when AI treats fundamentally different concerns as if they were the same thing.

The Pattern

┌────────────────────────────────────────────┐
│  "Store data"                              │
│       ↓                                    │
│  AI uses SQLite everywhere                 │
│       ↓                                    │
│  Category Error: Conflates local           │
│  storage with cloud storage                │
└────────────────────────────────────────────┘

Seven Category Errors We Encountered

#AI SeesAI DoesCategory Error
1"Store data"Uses SQLiteLocal ≠ Cloud
2"Add embeddings"Installs PyTorchInference ≠ Training
3"Cache this"Process memoryStateless ≠ Stateful
4"Deploy"Remote buildDev ≠ Production
5"Add analytics"Direct API callsFirst-party ≠ Third-party
6"Check permissions"Endpoint checksAuth ≠ Authorization
7"Store config"Environment varsStructured ≠ Strings

Each produces working code that fails when the underlying assumption is violated.


Category Error #1

"Store Data" → SQLite Everywhere

The MVP Trap: AI sees "database" as a single concept.

What AI Did

# One database abstraction, used everywhere
class Storage:
    def __init__(self):
        self.db = sqlite3.connect("context.db")

Works in development. Fails when you need:

  • Concurrent multi-user writes
  • Vector embeddings (pgvector)
  • Distributed transactions

The Missing Abstraction

┌─────────────────────────────────────────────┐
│              Storage Router                  │
├──────────────────┬──────────────────────────┤
│  LocalStorage    │    CloudStorage          │
│  (SQLite)        │    (PostgreSQL)          │
│                  │                          │
│  • Single user   │    • Multi-tenant        │
│  • Offline-first │    • Always-online       │
│  • Lightweight   │    • Scalable            │
└──────────────────┴──────────────────────────┘

Impact

MetricValue
Time to fix after coupling~2 days
Time if abstracted upfront~2 hours
Abstraction tax10x

Category Error #2

"Add Embeddings" → Install PyTorch

The MVP Trap: AI conflates "embedding model" with "full ML framework."

What AI Did

from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")

Result:

  • 3GB of dependencies
  • 270-second Docker builds
  • GPU/CPU confusion

The Missing Abstraction

class EmbeddingBackend(Protocol):
    def embed(self, texts: list[str]) -> list[list[float]]: ...

# Light option (default)
class FastEmbedBackend(EmbeddingBackend): ...  # 500MB

# Heavy option (opt-in)
class TorchBackend(EmbeddingBackend): ...  # 3GB

Impact

MetricBeforeAfterImprovement
Install size3GB500MB6x
Docker build270s51s5x
ComplexityHighLowMuch simpler

Category Error #3

"Cache for Performance" → Process Memory

The MVP Trap: AI treats caching as a local optimization.

What AI Did

class MCPServer:
    def __init__(self):
        # Cached at startup!
        self.collection = chroma.get_collection("memories")

Works until the database is rebuilt. Then: "Collection does not exist" errors that look like corruption.

The Missing Abstraction

# Connection factory, not cached connection
class CollectionFactory:
    def get_collection(self) -> Collection:
        return self.client.get_collection("memories")

Impact

BeforeAfter
Hours debugging per incident5 minutes
Improvement: 24x

Category Error #4

"Deploy the App" → Remote Build

The MVP Trap: AI sees deployment as "run the build somewhere else."

What AI Did

railway up  # Push source, build remotely

Result:

  • 30% failure rate (timeouts)
  • Inconsistent builds
  • Slow debugging

The Missing Abstraction

Source Code  →  Docker Image  →  Production
  (Git)         (Pre-built)      (Instant)
   │                │                │
 BUILD           ARTIFACT         DEPLOY
 (CI/CD)        (Registry)       (Railway)

Impact

ApproachReliability
Remote build70%
Pre-built image98%

Category Error #5

"Add Analytics" → Direct Third-Party Calls

The MVP Trap: AI treats third-party services as first-party endpoints.

What AI Did

posthog.init('key', {
  api_host: 'https://us.i.posthog.com'
})

Problem: VPNs and ad blockers block us.i.posthog.com.

The Missing Abstraction

Your Domain
┌────────────────────────────────────┐
│  /ingest/*  →  us.i.posthog.com   │
└────────────────────────────────────┘
         ↑
    Users see YOUR domain
    Blockers don't block YOU

The Fix

// 5 lines AI wouldn't think to add
rewrites: [{
  source: '/ingest/:path*',
  destination: 'https://us.i.posthog.com/:path*'
}]

Category Error #6

"Check Permissions" → Endpoint-Level Checks

The MVP Trap: AI adds checks where you ask, not where they belong.

What AI Did

async def get_memory(id: str, user: User):
    memory = await db.get_memory(id)
    if memory.user_id != user.id:  # Manual check
        raise HTTPException(403)

One missed endpoint = data leak.

The Missing Abstraction

class TenantScopedRepository:
    def __init__(self, user_id: str):
        self.user_id = user_id

    async def get_memory(self, id: str):
        # Isolation IMPOSSIBLE to forget
        return await db.query(Memory).filter(
            Memory.id == id,
            Memory.user_id == self.user_id
        ).first()

Impact

ApproachSecurityBurden
Endpoint checksFragileMust remember
Query-layer scopeRobustZero

Category Error #7

"Store Config" → Environment Variables

The MVP Trap: AI treats all configuration as string key-value pairs.

What AI Did

TYPE_SCHEMAS = json.loads(os.environ.get("TYPE_SCHEMAS", "{}"))

No validation. No types. No IDE support.

The Missing Abstraction

class DecisionData(BaseModel):
    decision: str        # Required
    rationale: str       # Required
    alternatives: list[str] = []

# Type-safe usage
def process(data: DecisionData):
    print(data.decision)  # IDE knows this

Impact

Result: 22 typed schemas, 71 tests, zero runtime type errors.


The Meta-Pattern

Every category error follows the same structure:

┌─────────────────────────────────────────┐
│  Request: "Do X"                        │
│            ↓                            │
│  AI: Direct implementation              │
│            ↓                            │
│  Works: Yes (MVP achieved)              │
│  Missing: Abstraction layer             │
│            ↓                            │
│  Result: Tech debt when concerns        │
│          diverge                        │
└─────────────────────────────────────────┘

AI optimizes for "does it work?" not "will it scale?"


Decision Framework

When to Override AI's MVP Instincts

AI SuggestsAsk YourselfOverride If
Single databaseDifferent requirements for local/cloud?Yes → Separate layers
Heavy dependencyNeed full capability?No → Lighter alternative
In-memory cacheLong-running process?Yes → Connection factory
Direct deployNeed reproducibility?Yes → Pre-build artifacts
Direct third-partyUsers might have blockers?Yes → Reverse proxy
Endpoint checksCross-cutting concern?Yes → Query-layer
String configHas structure?Yes → Type it

Quantified Impact

Abstraction Tax: Before vs After

CategoryMVP CostFix TimeIf Upfront
Database split2 days2 days2 hours
Embedding backendOngoing4 hours1 hour
Cache invalidation2 hrs/incident15 min30 min
Deployment30% failures1 day2 hours
Third-party proxy4 hours debug30 min15 min
Tenant isolationAudit time1 day4 hours
Typed configWeeksWeeks1 day

Summary

Total abstraction tax paidIf architected upfront
~2 weeks~2 days

The Eight Abstractions AI Will Skip

  1. Storage topology — Local vs. cloud are different categories
  2. Inference backend — Separate model from framework
  3. Cache lifecycle — Explicit invalidation, not implicit
  4. Build artifacts — Separate build from deploy
  5. First-party boundary — Proxy third-party through your domain
  6. Tenant scope — Enforce at query layer, not endpoint
  7. Type system — Structure your structured data
  8. Configuration layer — Separate what changes from what doesn't

Conclusion

AI as Architect's Assistant, Not Architect

AI coding assistants are extraordinarily good at implementing what you ask for.

They are systematically bad at asking "should this be abstracted?"

The solution isn't less AI—it's using AI within an architectural frame you provide.

The Pattern

AI builds MVPs. Humans build architectures. The best results come from knowing when AI's MVP instinct needs a proper abstraction layer.

The Recursive Insight

ContextFS was built almost entirely with AI assistance. The issues here weren't AI failures—they were human failures to provide architectural constraints.

We now use ContextFS itself to remember these patterns. Future AI sessions start with abstractions already defined.

Every problem solved becomes searchable knowledge for future problems.


ContextFS is persistent memory infrastructure for AI coding agents—built with AI, for AI.

Get Started Free → · Read the Docs →