Status: Accepted
Date: 2025-12-28
Deciders: Product & Engineering Team
Software architecture can be:
- Monolithic (tightly coupled)
- Service-oriented (interfaces)
- Microservices (fully distributed)
Product Question: How do we structure code for long-term flexibility and maintainability?
Use interface-based modular architecture with dependency injection.
Implementation:
- All services implement Protocol interfaces
- Factory functions for instantiation
- Plug-and-play implementations
- Single Responsibility Principle
-
Plug-and-Play Upgrades
# Change implementation without code changes gate = create_approval_gate("simple") # MVP gate = create_approval_gate("langgraph") # Future
-
Easy Testing
# Mock implementations for unit tests mock_gate = MockApprovalGate() service = RewriteAgent(approval_gate=mock_gate)
-
Independent Evolution
- Upgrade ApprovalGate without touching RewriteAgent
- Swap LLM providers without rewriting services
- Replace storage backends transparently
-
SYSTEM.md Section 4 Compliance
- Single responsibility ✓
- Independently deployable ✓
- No hidden dependencies ✓
- Versioned interfaces ✓
-
Future-Proof
- Today: Simple in-memory implementations
- Tomorrow: Database-backed, distributed, cloud-native
- Zero service code changes
from typing import Protocol
class IApprovalGate(Protocol):
"""Abstract interface - ANY implementation works"""
def request_approval(...) -> ApprovalRequest: ...
def approve(...) -> HITLDecision: ...
def reject(...) -> HITLDecision: ...class SimpleApprovalGate(IApprovalGate):
"""MVP: In-memory storage"""
class DatabaseApprovalGate(IApprovalGate):
"""Production: PostgreSQL storage"""
class LangGraphApprovalGate(IApprovalGate):
"""Future: Multi-agent workflows"""def create_approval_gate(impl="simple") -> IApprovalGate:
"""Configuration-based instantiation"""
if impl == "simple":
return SimpleApprovalGate()
elif impl == "database":
return DatabaseApprovalGate(db_url=DB_URL)
elif impl == "langgraph":
return LangGraphApprovalGate(graph=...)class RewriteAgent:
def __init__(self, approval_gate: IApprovalGate):
self.approval_gate = approval_gate # ANY implementation!
def suggest_improvements(self, resume):
# Works with SimpleGate, DatabaseGate, LangGraphGate...
approval = self.approval_gate.request_approval(...)✅ Flexibility: Swap implementations via config
✅ Testability: Mock interfaces for unit tests
✅ Maintainability: Change one component independently
✅ Scalability: Upgrade storage/logic without rewrites
✅ Team Productivity: Multiple devs work on different implementations
- Clear documentation and examples
- Factory functions hide complexity
- ADRs explain decisions
Example:
class ApprovalGate:
def __init__(self):
self.storage = {} # Hardcoded
self.workflow = "simple" # HardcodedPros:
- Simple to start
Cons:
- Cannot swap implementations
- Hard to test (no mocks)
- Tight coupling
- Violates SYSTEM.md Section 4
Rejected: Not maintainable long-term
Pros:
- Ultimate decoupling
- Independent deployment
Cons:
- Overkill for MVP
- Network latency
- Deployment complexity
- Debugging harder
Deferred: Consider for v2.0 at scale
Pros:
- Right-sized for current scale
- Plug-and-play upgrades
- Easy testing
- SYSTEM.md compliant
- Path to microservices if needed
Cons:
- Slightly more initial code
Accepted: Best balance for MVP with growth path
All VeriFit services follow this pattern:
| Service | Interface | Implementations |
|---|---|---|
| Resume Parser | IResumeParser |
ResumeParser (deterministic), future: LLMResumeParser |
| Job Parser | - (internal) | JobParser (deterministic + LLM fallback) |
| Approval Gate | IApprovalGate |
SimpleApprovalGate, future: LangGraphApprovalGate |
| Job Matcher | - (internal) | JobMatcher (deterministic scoring) |
# Swap implementations via config
APPROVAL_GATE_IMPL=simple # MVP
# APPROVAL_GATE_IMPL=langgraph # Future
# APPROVAL_GATE_IMPL=database # Production
STORAGE_BACKEND=memory # MVP
# STORAGE_BACKEND=postgresql # Production# main.py
from config import APPROVAL_GATE_IMPL
gate = create_approval_gate(APPROVAL_GATE_IMPL)
rewriter = RewriteAgent(approval_gate=gate)
# Zero code changes to swap implementations!MVP: Fast iteration with simple implementations
Growth: Upgrade to production backends without rewrites
Scale: Swap to distributed systems via config
- Frontend team: Mocks for testing
- Backend team: Swap storage independently
- ML team: Experiment with different LLMs
- Local: In-memory implementations
- Staging: Simple database
- Production: Distributed, cached, optimized
One codebase, multiple deployment modes
- All services use interface pattern
- Factory functions for instantiation
- Zero service-to-service tight coupling
- Can swap implementations via config
- Mock implementations for all tests
- Interfaces Cost Time Upfront, Save Time Long-Term
- Python Protocol is Perfect for This (runtime checkable)
- Factory Pattern Hides Complexity from users
- SYSTEM.md Section 4 provides excellent guidance
- SYSTEM.md Section 4
- Approval Gate Implementation
- Factory Pattern Examples
- Python Protocol Documentation
Review Date: Quarterly - assess if microservices needed
Status: Monitor coupling - refactor if interfaces violated