AI System Design for Product Constraints
Design AI systems with explicit latency, cost, reliability, and governance tradeoffs.
In a demo, you control everything: the prompt, the latency, the cost, the data. In production, you operate under a set of constraints that were decided before you wrote a line of code. The engineers who ship AI features sustainably are the ones who treat these constraints as first-class design inputs, not obstacles to work around.
This article covers the five constraints that most commonly get skipped in demos but always matter in production.
1. Latency SLAs
Your product has a response time budget. Find out what it is before you design your AI feature.
pythonimport time import statistics from typing import Callable def measure_latency(fn: Callable, inputs: list, percentiles=(50, 95, 99)) -> dict: latencies = [] for inp in inputs: start = time.perf_counter() fn(inp) latencies.append((time.perf_counter() - start) * 1000) latencies.sort() return { f"p{p}": latencies[int(len(latencies) * p / 100)] for p in percentiles } # Typical web SLAs: # Interactive UI feature: p95 < 500ms # Background enrichment: p95 < 5s # Async report: p95 < 30s # If your LLM call is 2s and your SLA is 500ms, you have four options: # 1. Stream the response (perceived latency improvement, not real) # 2. Pre-compute and cache for predictable inputs # 3. Move the feature to async / background # 4. Use a faster (smaller) model
Streaming is the most common technique. It shifts the user experience from "wait 2 seconds, then see everything" to "see the first word in 200ms, full response in 2 seconds." The actual latency is the same.
2. Cost Budgets
LLM costs scale with usage in a way that database costs do not. Establish a cost budget per feature before launch.
pythonclass CostBudget: def __init__(self, monthly_budget_usd: float, feature: str): self.budget = monthly_budget_usd self.feature = feature self.spent = self._load_current_spend() def check(self, estimated_cost: float) -> bool: """Returns True if this call is within budget.""" if self.spent + estimated_cost > self.budget: log.warning(f"Feature {self.feature} approaching budget limit " f"(${self.spent:.2f} / ${self.budget:.2f})") return False return True def record(self, actual_cost: float): self.spent += actual_cost self._persist(actual_cost)
Rule of thumb: prototype with GPT-4o, ship with GPT-4o-mini or equivalent. The 10x cost difference matters at scale. Benchmark quality on your specific task before switching - for many classification and extraction tasks, the difference is negligible.
3. Privacy and Data Residency
Many AI APIs send your data to third-party servers. This is a compliance issue, not just a preference.
pythondef redact_pii(text: str) -> tuple[str, dict]: """Redact PII before sending to external LLM API.""" import re replacements = {} counter = 0 patterns = { "email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', "phone": r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', "ssn": r'\b\d{3}-\d{2}-\d{4}\b', "card": r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', } for pii_type, pattern in patterns.items(): for match in re.finditer(pattern, text): redacted_token = f"[{pii_type.upper()}_{counter}]" replacements[redacted_token] = match.group() text = text.replace(match.group(), redacted_token) counter += 1 return text, replacements # restore replacements in post-processing
For heavily regulated environments (healthcare, finance), consider self-hosted models. The operational overhead is real, but it eliminates the data residency problem entirely.
4. Audit Trails
For any AI feature that affects a user outcome (a decision, a recommendation, a content moderation action), you need a complete audit trail.
pythonfrom dataclasses import dataclass, asdict from datetime import datetime, timezone import uuid @dataclass class LLMAuditRecord: record_id: str feature: str user_id: str timestamp: str model: str prompt_hash: str # hash of prompt, not raw prompt (may contain PII) response_summary: str # truncated, sanitized input_tokens: int output_tokens: int latency_ms: float decision: str | None # if this was a decision-making call def audit_llm_call(feature, user_id, prompt, response, usage, latency_ms, decision=None): import hashlib record = LLMAuditRecord( record_id=str(uuid.uuid4()), feature=feature, user_id=user_id, timestamp=datetime.now(timezone.utc).isoformat(), model=response.model, prompt_hash=hashlib.sha256(prompt.encode()).hexdigest()[:16], response_summary=response.choices[0].message.content[:200], input_tokens=usage.prompt_tokens, output_tokens=usage.completion_tokens, latency_ms=latency_ms, decision=decision, ) audit_store.write(asdict(record))
5. Rate Limits
Your LLM provider has rate limits. Your own API probably has rate limits for the AI features. Model both.
pythonfrom asyncio import Semaphore class RateLimitedLLMClient: def __init__(self, requests_per_minute: int = 60): self.semaphore = Semaphore(requests_per_minute // 10) self.rpm = requests_per_minute async def call(self, prompt: str) -> str: async with self.semaphore: return await self._call_with_retry(prompt)
Common Mistakes
Designing for average load. Your latency budget must hold at p95 under production traffic, not average traffic. Test with concurrent load.
Ignoring egress costs. Many cloud providers charge for data leaving a region. If your self-hosted model is in us-east-1 and your application is in eu-west-1, you are paying egress on every model call.
No cost alerting. Set a billing alert at 80% of your monthly LLM budget. You will hit it.
Where to Go Next
See also: [ai-system-design-reliability], [llm-app-engineering-production], [tool-using-agents-guardrails]
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsFine-Tuning and Post-Training: LoRA, SFT, DPO, and Reasoning RL
What actually happens after pretraining, and when you should do any of it yourself. Parameter-efficient fine-tuning with LoRA, supervised fine-tuning data, preference optimization, and the reinforcement learning recipe behind reasoning models, with a decision framework and a project you can run on one GPU.
LLM Context Windows: What They Mean for System Design
Context window size shapes every architectural decision in LLM applications. This post covers how to reason about context allocation, the limits that still matter even with large windows, and the patterns that scale.
Common ML Architectures Reference: CNN, RNN, Transformer, MoE
A concise technical reference for the neural network architectures that power modern ML - what each one does, how it works, when to use it, and what to watch out for.