Architecting for Decay: Building Self-Maintaining Systems in the Age of AI-Generated Code
Originally published on tamiz.pro . The paradigm of software development is undergoing a silent but profound structural shift. For decades, the cost of writing code has been the primary bottleneck in engineering. Today, large language models (LLMs) and AI coding assistants have drastically reduced

Posted on Oct 15 • Originally published at tamiz.pro
The paradigm of software development is undergoing a silent but profound structural shift. For decades, the cost of writing code has been the primary bottleneck in engineering. Today, large language models (LLMs) and AI coding assistants have drastically reduced the marginal cost of code generation. We are entering an era where code is written at the speed of thought, but the cost of maintaining that code—debugging, refactoring, documenting, and understanding the side effects—remains stubbornly high for human engineers.
This asymmetry creates a new class of technical debt: Maintenance Churn. If we continue to design systems with the assumption that code is scarce and precious, we will drown in a tsunami of fragile, over-engineered, or poorly integrated logic that AI produces with ease. To survive the AI era, we must stop designing for longevity through rigidity and start designing for planned obsolescence and rapid decomposition. This deep dive explores the architectural principles of "Design for Churn"—how to structure systems so that AI-generated code can be generated, deployed, tested, and discarded without destabilizing the core platform.
Traditional software engineering relies on the assumption that the cost of a line of code is high. Therefore, engineers invest heavily in abstraction, documentation, and code review to ensure that the system remains maintainable over years. This works when code is written slowly.
In the AI era, the input rate of code into the repository has increased by orders of magnitude. An engineer using an LLM agent can generate a complete microservice in minutes. However, the context window of an LLM is finite, and its understanding of the global system is partial. AI generates local solutions that are syntactically correct but often architecturally inconsistent with the existing codebase.
The result is churn. Features are generated, deployed, and then require constant refactoring because the AI does not "care" about the existing patterns. If the architecture is rigid, every AI-generated change breaks the compile or the integration tests. If the architecture is loose, the system becomes a Rube Goldberg machine of incompatible components.
We must invert the traditional mindset. Instead of asking, "How can I make this code last forever?", we must ask, "How can I make this code easy to delete?"
This approach, Design for Churn, prioritizes:
In a high-churn environment, modules should be treated as disposable artifacts. A module is a self-contained unit of functionality that can be regenerated by an AI agent if it breaks. For a module to be disposable, it must have a strict boundary. It should not reach across into other modules to fetch data or execute logic.
This aligns with the Dependency Inversion Principle (DIP), but with a stronger emphasis on the consumer defining the interface. The AI is better at implementing a specific, well-defined interface than it is at figuring out which existing class to modify.
AI models struggle with implicit behaviors. Magic methods, dynamic dispatch, and side-effects within getters/setters are the enemies of churn. If a piece of code relies on the absence of a function to behave correctly, or on a specific order of initialization, it will likely break when an AI agent rewrites the component.
Design for Churn demands explicit configuration. All behaviors must be driven by data or clearly defined interfaces, not by hidden state.
Since AI might generate multiple attempts to solve a problem, or retry a process after a partial failure, all external actions (database writes, API calls) must be idempotent. If the system cannot handle duplicate requests gracefully, the churn will lead to data corruption.
The most effective way to manage AI-generated code is to remove the AI from the loop of designing the interfaces and let it focus on implementing them. This is the Contract-First strategy.
Instead of letting the AI write the TypeScript interfaces or the gRPC.proto files, you define these contracts manually or with a very small, curated set of LLM calls. These contracts are the "truth" of the system.
Once the contract is defined, the AI can generate the implementation. Because the contract is explicit, the AI knows exactly what it needs to produce.
// The Contract (Defined by Human/Architect) interface PaymentProcessor { processPayment ( invoice: Invoice, token: string ): Promise < PaymentResult >; refundPayment ( paymentId: string, amount: number ): Promise < RefundResult >; } // The Implementation (Generated by AI) // AI knows it must implement 'processPayment' and 'refundPayment' exactly. // It does not know or care about the internal details of the other modules. Enter fullscreen mode Exit fullscreen mode The Anti-Pattern: AI-Generated Interfaces A common mistake is allowing the AI to generate the interfaces for new features. LLMs tend to over-generalize.
They will create interfaces that are too broad, including methods that are never used, or too narrow, requiring changes to other parts of the system. When interfaces change, the "churn" cascades.
By keeping interfaces stable and small, you contain the churn. If a new requirement arises, you don't modify the existing PaymentProcessor interface; you create a new CreditCardProcessor or WalletProcessor that inherits or implements a common base. This allows the old AI-generated code to remain valid until it is explicitly discarded.
Domain-Driven Design (DDD) is not just a methodology for business logic; in the AI era, it is a containment strategy.
A Bounded Context is the most important architectural tool for churn. It defines the logical boundary where a specific model is valid. For example, the concept of a "Customer" in the Sales context (name, email, credit limit) is different from a "Customer" in the Logistics context (address, tracking ID).
In a high-churn environment, you must strictly enforce these boundaries. AI agents work best when their context window is focused. If you ask an AI to modify the Customer entity, it should only see the code within the Sales context. It should not see the Logistics code.
This isolation ensures that when the AI regenerates the Sales module, it does not break the Logistics module. The two modules communicate via events (e.g., CustomerRegistered ), not direct function calls.
When using events to decouple modules, adopt Event Sourcing as the storage mechanism. An Event Sourced system stores the sequence of state changes rather than the current state.
or refactoring away dependencies that downstream agents rely on. When the source of truth is a linear log, "correction" becomes a new event that supersedes the old one, not a deletion. This auditability is crucial when an LLM hallucinates a fix; the history shows exactly what it claimed to change and why.
In a self-maintaining system, the AI doesn't just execute code; it must orchestrate the maintenance lifecycle. We introduce a layer we call the Conductor. The Conductor is not a single model but a state machine that decides who acts, when, and how much autonomy they have.
Consider a typical maintenance cycle for a high-traffic API endpoint:
Here is a simplified Python implementation of the Conductor’s decision logic. Note how we treat the agents as isolated functions that return structured data, keeping the orchestration deterministic.
import json import time from dataclasses import dataclass from enum import Enum from typing import Optional class MaintenancePhase ( Enum ): MONITORING = " monitoring " DIAGNOSING = " diagnosing " REMEDIATING = " remediating " VALIDATING = " validating " AWAITING_APPROVAL = " awaiting_approval " MERGING = " merging " CLOSED = " closed " @dataclass class MaintenanceTicket: ticket_id: str phase: MaintenancePhase hypothesis: Optional [ dict ] = None pull_request_url: Optional [ str ] = None validation_report: Optional [ dict ] = None risk_score: float = 0.0 class AgentConductor: def __init__ ( self, risk_threshold: float = 0.7 ): self. risk_threshold = risk_threshold # In production, these would be service clients or LLM wrappers self. diagnostic_agent = MockDiagnosticAgent () self. remediation_agent = MockRemediationAgent () self. validation_agent = MockValidationAgent () def process_incident ( self, incident_data: dict ) -> MaintenanceTicket: ticket = MaintenanceTicket ( ticket_id = incident_data. get ( " id ", " unknown " ), phase = MaintenancePhase. MONITORING ) # 1. Diagnose ticket. phase = MaintenancePhase.
DIAGNOSING diagnosis = self. diagnostic_agent. analyze ( incident_data ) ticket. hypothesis = diagnosis ticket. risk_score = diagnosis. get ( " risk ", 0.0 ) if ticket. risk_score < 0.3: # Low risk: Auto-remediate self. _execute_remediation ( ticket ) elif ticket. risk_score < self. risk_threshold: # Medium risk: Auto-validate, request human approval self. _execute_remediation ( ticket ) self. _execute_validation ( ticket ) ticket. phase = MaintenancePhase. AWAITING_APPROVAL # Notification service would trigger here self. _request_human_approval ( ticket ) else: # High risk: Human-led intervention ticket. phase = MaintenancePhase. AWAITING_APPROVAL self. _notify_engineers ( ticket ) return ticket def _execute_remediation ( self, ticket: MaintenanceTicket ): ticket. phase = MaintenancePhase.
REMEDIATING pr_info = self. remediation_agent. generate_fix ( ticket. hypothesis ) ticket. pull_request_url = pr_info. get ( " url " ) def _execute_validation ( self, ticket: MaintenanceTicket ): ticket. phase = MaintenancePhase. VALIDATING ticket. validation_report = self. validation_agent. run_tests ( ticket. pull_request_url ) if not ticket. validation_report. get ( " passed ", False ): ticket. phase = MaintenancePhase. CLOSED self. _log_failure ( ticket ) def _request_human_approval ( self, ticket: MaintenanceTicket ): # In a real system, this sends to Slack/PagerDuty print ( f " [HITL] Requesting approval for { ticket. ticket_id } (Risk: { ticket. risk_score } ) " ) # Simulate immediate approval for demo time. sleep ( 1 ) self. _execute_merge ( ticket ) def _execute_merge ( self, ticket: MaintenanceTicket ): ticket. phase = MaintenancePhase.
MERGING # Git integration logic here ticket. phase = MaintenancePhase. CLOSED print ( f " [SUCCESS] Merged fix for { ticket. ticket_id } " ) def _notify_engineers ( self, ticket: MaintenanceTicket ): print ( f " [ESCALATION] High-risk incident { ticket. ticket_id }. Manual intervention required.
" ) # Mock agents for demonstration class MockDiagnosticAgent: def analyze ( self, data: dict ) -> dict: return { " type ": " n_plus_one_query ", " risk ": 0.2, " module ": " users " } class MockRemediationAgent: def generate_fix ( self, hypothesis: dict ) -> dict: return { " url ": " https://git.example.com/PR/1234 " } class MockValidationAgent: def run_tests ( self, pr_url: str ) -> dict: return { " passed ": True, " coverage_delta ": + 0.5 } Enter fullscreen mode Exit fullscreen mode This pattern ensures that the "decay" (technical debt, bug accumulation) is countered by a continuous, automated loop that scales with the system's complexity, rather than relying on manual code reviews that become bottlenecks.
As AI agents generate more code, the largest risk isn't syntax errors—it's semantic drift. An agent might "optimize" a function by removing a specific edge-case check because the tests didn't cover it, subtly changing the business logic. To prevent this, we define a Code Constitution: a set of immutable, high-level invariants that the system must uphold.
The Constitution is not a linter; it is a formal specification. We store these in a structured format (like OpenAPI or custom JSON schemas) that both the AI and the CI/CD pipeline can verify.
Suppose our system guarantees that Order.Total is always equal to sum(Item.Price * Item.Quantity). This is a business invariant.
{ "invariant_id": "INV-ORDER-01", "description": "Order total must match itemized sum", "rule": "ORDER.total == SUM(ITEM.price * ITEM.quantity) FOR ALL ITEM IN ORDER.items", "criticality": "HIGH", "validation": "unit_test", "agent_permission": "READ_ONLY_CONTEXT" } Enter fullscreen mode Exit fullscreen mode When an AI agent proposes a change to Order.calculate_total(), the Conductor’s Validation Agent doesn't just run tests; it runs Invariant Checks. These are specific property-based tests generated from the Constitution. If the AI agent's new implementation fails INV-ORDER-01 even if all existing unit tests pass, the PR is rejected. The agent is then prompted to "Explain why this invariant might no longer be valid" or "Propose a way to maintain this invariant."
This forces the AI to align with the intent of the system, not just the current state of the code.
LLMs have limited context windows. A self-maintaining system needs "long-term memory" to remember past failures, successful fixes, and architectural decisions. We implement this using a vector database coupled with a knowledge graph.
When a new incident occurs, the Diagnostic Agent retrieves similar past episodes from the vector DB to bootstrap its hypothesis. This reduces hallucination and grounds the agent in historical reality.
Architecting for decay is not about building systems that never rot. It is about building systems that self-heal at a pace faster than they decay. By treating code as an event-sourced ledger, orchestrating AI agents through deterministic state machines, enforcing semantic invariants, and grounding agents in historical memory, we shift the engineer's role from "code writer" to "system architect" and "agent supervisor."
The future of software maintenance is not human-centric; it is agent-centric. The goal is to make the maintenance loop so tight and reliable that the system becomes self-maintaining, leaving humans free to focus on high-level strategy and innovation, rather than the mundane task of keeping the lights on.
Start small. Pick one module with high change frequency. Apply the Conductor pattern. Let the AI maintain it. Observe the decay rate. When the decay rate drops below the repair rate, you have built a self-maintaining system.