This document outlines the core implementation philosophy and guidelines for software development projects. It serves as a central reference for decision-making and development approach throughout the project.
Embodies a Zen-like minimalism that values simplicity and clarity above all. This approach reflects:
- Wabi-sabi philosophy: Embracing simplicity and the essential. Each line serves a clear purpose without unnecessary embellishment.
- Occam's Razor thinking: The solution should be as simple as possible, but no simpler.
- Trust in emergence: Complex systems work best when built from simple, well-defined components that do one thing well.
- Present-moment focus: The code handles what's needed now rather than anticipating every possible future scenario.
- Pragmatic trust: The developer trusts external systems enough to interact with them directly, handling failures as they occur rather than assuming they'll happen.
This development philosophy values clear documentation, readable code, and belief that good architecture emerges from simplicity rather than being imposed through complexity.
- KISS principle taken to heart: Keep everything as simple as possible, but no simpler
- Minimize abstractions: Every layer of abstraction must justify its existence
- Start minimal, grow as needed: Begin with the simplest implementation that meets current needs
- Avoid future-proofing: Don't build for hypothetical future requirements
- Question everything: Regularly challenge complexity in the codebase
- Preserve key architectural patterns: MCP for service communication, SSE for events, separate I/O channels, etc.
- Simplify implementations: Maintain pattern benefits with dramatically simpler code
- Scrappy but structured: Lightweight implementations of solid architectural foundations
- End-to-end thinking: Focus on complete flows rather than perfect components
Choosing between custom code and external libraries is a judgment call that evolves with your requirements. There's no rigid rule - it's about understanding trade-offs and being willing to revisit decisions as needs change.
Your approach might naturally evolve:
- Start simple: Custom code for basic needs (20 lines handles it)
- Growing complexity: Switch to a library when requirements expand
- Hitting limits: Back to custom when you outgrow the library's capabilities
This isn't failure - it's natural evolution. Each stage was the right choice at that time.
Custom code often wins when:
- The need is simple and well-understood
- You want code perfectly tuned to your exact requirements
- Libraries would require significant "hacking" or workarounds
- The problem is unique to your domain
- You need full control over the implementation
Libraries shine when:
- They solve complex problems you'd rather not tackle (auth, crypto, video encoding)
- They align well with your needs without major modifications
- The problem is well-solved with mature, battle-tested solutions
- Configuration alone can adapt them to your requirements
- The complexity they handle far exceeds the integration cost
Ask yourself:
- How well does this library align with our actual needs?
- Are we fighting the library or working with it?
- Is the integration clean or does it require workarounds?
- Will our future requirements likely stay within this library's capabilities?
- Is the problem complex enough to justify the dependency?
Watch for signs you're fighting your current approach:
- Spending more time working around the library than using it
- Your simple custom solution has grown complex and fragile
- You're monkey-patching or heavily wrapping a library
- The library's assumptions fundamentally conflict with your needs
Remember that complexity isn't destroyed, only moved. Libraries shift complexity from your code to someone else's - that's often a great trade, but recognize what you're doing.
The key is avoiding lock-in. Keep library integration points minimal and isolated so you can switch approaches when needed. There's no shame in moving from custom to library or library to custom. Requirements change, understanding deepens, and the right answer today might not be the right answer tomorrow. Make the best decision with current information, and be ready to evolve.
- Implement only essential endpoints
- Minimal middleware with focused validation
- Clear error responses with useful messages
- Consistent patterns across endpoints
- Simple schema focused on current needs
- Use TEXT/JSON fields to avoid excessive normalization early
- Add indexes only when needed for performance
- Delay complex database features until required
- Streamlined MCP client with minimal error handling
- Utilize FastMCP when possible, falling back to lower-level only when necessary
- Focus on core functionality without elaborate state management
- Simplified connection lifecycle with basic error recovery
- Implement only essential health checks
- Basic SSE connection management
- Simple resource-based subscriptions
- Direct event delivery without complex routing
- Minimal state tracking for connections
- Simple topic-based publisher/subscriber
- Direct event delivery without complex pattern matching
- Clear, minimal event payloads
- Basic error handling for subscribers
- Direct integration with PydanticAI
- Minimal transformation of responses
- Handle common error cases only
- Skip elaborate caching initially
- Simplified queue-based processing
- Direct, focused routing logic
- Basic routing decisions without excessive action types
- Simple integration with other components
- Implement complete end-to-end functionality slices
- Start with core user journeys
- Get data flowing through all layers early
- Add features horizontally only after core flows work
- 80/20 principle: Focus on high-value, low-effort features first
- One working feature > multiple partial features
- Validate with real usage before enhancing
- Be willing to refactor early work as patterns emerge
- Emphasis on integration and end-to-end tests
- Manual testability as a design goal
- Focus on critical path testing initially
- Add unit tests for complex logic and edge cases
- Testing pyramid: 60% unit, 30% integration, 10% end-to-end
- Handle common errors robustly
- Log detailed information for debugging
- Provide clear error messages to users
- Fail fast and visibly during development
When faced with implementation decisions, ask these questions:
- Necessity: "Do we actually need this right now?"
- Simplicity: "What's the simplest way to solve this problem?"
- Directness: "Can we solve this more directly?"
- Value: "Does the complexity add proportional value?"
- Maintenance: "How easy will this be to understand and change later?"
Some areas justify additional complexity:
- Security: Never compromise on security fundamentals
- Data integrity: Ensure data consistency and reliability
- Core user experience: Make the primary user flows smooth and reliable
- Error visibility: Make problems obvious and diagnosable
Push for extreme simplicity in these areas:
- Internal abstractions: Minimize layers between components
- Generic "future-proof" code: Resist solving non-existent problems
- Edge case handling: Handle the common cases well first
- Framework usage: Use only what you need from frameworks
- State management: Keep state simple and explicit
# Simple, focused SSE manager that does exactly what's needed
class SseManager:
def __init__(self):
self.connections = {} # Simple dictionary tracking
async def add_connection(self, resource_id, user_id):
"""Add a new SSE connection"""
connection_id = str(uuid.uuid4())
queue = asyncio.Queue()
self.connections[connection_id] = {
"resource_id": resource_id,
"user_id": user_id,
"queue": queue
}
return queue, connection_id
async def send_event(self, resource_id, event_type, data):
"""Send an event to all connections for a resource"""
# Direct delivery to relevant connections only
for conn_id, conn in self.connections.items():
if conn["resource_id"] == resource_id:
await conn["queue"].put({
"event": event_type,
"data": data
})# Overly complex with unnecessary abstractions and state tracking
class ConnectionRegistry:
def __init__(self, metrics_collector, cleanup_interval=60):
self.connections_by_id = {}
self.connections_by_resource = defaultdict(list)
self.connections_by_user = defaultdict(list)
self.metrics_collector = metrics_collector
self.cleanup_task = asyncio.create_task(self._cleanup_loop(cleanup_interval))
# [50+ more lines of complex indexing and state management]# Focused MCP client with clean error handling
class McpClient:
def __init__(self, endpoint: str, service_name: str):
self.endpoint = endpoint
self.service_name = service_name
self.client = None
async def connect(self):
"""Connect to MCP server"""
if self.client is not None:
return # Already connected
try:
# Create SSE client context
async with sse_client(self.endpoint) as (read_stream, write_stream):
# Create client session
self.client = ClientSession(read_stream, write_stream)
# Initialize the client
await self.client.initialize()
except Exception as e:
self.client = None
raise RuntimeError(f"Failed to connect to {self.service_name}: {str(e)}")
async def call_tool(self, name: str, arguments: dict):
"""Call a tool on the MCP server"""
if not self.client:
await self.connect()
return await self.client.call_tool(name=name, arguments=arguments)# Complex MCP client with excessive state management and error handling
class EnhancedMcpClient:
def __init__(self, endpoint, service_name, retry_strategy, health_check_interval):
self.endpoint = endpoint
self.service_name = service_name
self.state = ConnectionState.DISCONNECTED
self.retry_strategy = retry_strategy
self.connection_attempts = 0
self.last_error = None
self.health_check_interval = health_check_interval
self.health_check_task = None
# [50+ more lines of complex state tracking and retry logic]- It's easier to add complexity later than to remove it
- Code you don't write has no bugs
- Favor clarity over cleverness
- The best code is often the simplest
This philosophy document serves as the foundational guide for all implementation decisions in the project.