Section 1.4.6
Examples: Good vs. Bad Interfaces
Digestible Interfaces
Examples: Good vs. Bad Interfaces
Theory is useful, but examples make principles concrete. In this section, we'll examine real-world interface patterns—the kind you encounter every day—and see how digestible design transforms them.
For each example, we'll examine:
- The problematic version and its issues
- Why it's hard for humans AND AI agents
- The improved version
- Why it works better for both audiences
Example 1: Function Signatures
The Problem: A function with too many parameters.
Bad: The Parameter Explosion
def create_user(
username,
email,
password,
first_name,
last_name,
date_of_birth,
phone_number,
address_line1,
address_line2,
city,
state,
postal_code,
country,
newsletter_opt_in,
terms_accepted,
privacy_accepted,
referral_code,
preferred_language,
timezone,
profile_image_url,
bio
):
"""Create a new user with the provided information."""
pass
Human problems:
- Can't remember parameter order without documentation
- Easy to swap similar parameters (city/state, first_name/last_name)
- Calling code is unreadable:
create_user("alice", "a@b.com", "secret", "Alice", "Smith", ...) - Adding a new parameter requires finding all call sites
AI agent problems:
- 21 parameters consume significant context explaining each one
- High probability of mixing up argument positions
- Agent may not know which parameters are optional
- Generated calls will be verbose and hard to verify
Good: Structured Parameter Objects
@dataclass
class UserProfile:
"""User's personal information."""
first_name: str
last_name: str
date_of_birth: date
bio: str = ""
profile_image_url: Optional[str] = None
@dataclass
class Address:
"""Physical address."""
line1: str
city: str
state: str
postal_code: str
country: str
line2: str = ""
@dataclass
class ContactInfo:
"""Contact information."""
email: str
phone: Optional[str] = None
@dataclass
class Preferences:
"""User preferences."""
language: str = "en"
timezone: str = "UTC"
newsletter_opt_in: bool = False
@dataclass
class CreateUserRequest:
"""Request to create a new user account."""
username: str
password: str
contact: ContactInfo
profile: UserProfile
address: Optional[Address] = None
preferences: Preferences = field(default_factory=Preferences)
referral_code: Optional[str] = None
def __post_init__(self):
if len(self.password) < 8:
raise ValidationError("Password must be at least 8 characters")
def create_user(request: CreateUserRequest) -> User:
"""Create a new user account.
Args:
request: Validated user creation request
Returns:
Created User with assigned ID
Raises:
ValidationError: If request data is invalid
DuplicateUserError: If username or email exists
"""
pass
Why this is better:
- For humans: Related fields are grouped logically. IDE autocomplete shows structure. Call sites are readable.
- For AI agents: Single parameter to understand. Type hints provide complete documentation. Validation is built-in.
Usage comparison:
# Bad: What do all these parameters mean?
create_user("alice", "a@b.com", "secret123", "Alice", "Smith",
date(1990, 1, 15), "555-1234", "123 Main St", "",
"Seattle", "WA", "98101", "US", True, True, True,
None, "en", "America/Los_Angeles", None, "Hello!")
# Good: Self-documenting structure
create_user(CreateUserRequest(
username="alice",
password="secret123",
contact=ContactInfo(email="a@b.com", phone="555-1234"),
profile=UserProfile(
first_name="Alice",
last_name="Smith",
date_of_birth=date(1990, 1, 15),
bio="Hello!"
),
address=Address(
line1="123 Main St",
city="Seattle",
state="WA",
postal_code="98101",
country="US"
)
))
Example 2: API Endpoints
The Problem: REST endpoints with unclear contracts.
Bad: Ambiguous API
# What does this endpoint accept? Return? When does it fail?
@app.route('/api/orders', methods=['POST'])
def create_order():
data = request.json
# Accepts... something
# Returns... something
# Might fail... somehow
pass
@app.route('/api/orders/<id>')
def get_order(id):
# What type is id? String? Int?
# What if order doesn't exist?
# What fields are returned?
pass
Problems for both audiences:
- No type information on inputs or outputs
- Error cases undocumented
- No way to know what fields to send or expect
Good: Explicit API Contract
from pydantic import BaseModel, Field
from typing import List
from decimal import Decimal
from datetime import datetime
# Request/Response models
class OrderItem(BaseModel):
"""Item in an order."""
product_id: int = Field(gt=0, description="Product ID")
quantity: int = Field(gt=0, le=100, description="Quantity")
class CreateOrderRequest(BaseModel):
"""Request to create an order."""
customer_id: int = Field(gt=0)
items: List[OrderItem] = Field(min_items=1, max_items=50)
shipping_address_id: int = Field(gt=0)
notes: str = Field(default="", max_length=500)
class OrderResponse(BaseModel):
"""Order data returned from API."""
id: int
customer_id: int
items: List[OrderItem]
total: Decimal
status: str = Field(pattern="^(pending|confirmed|shipped|delivered)$")
created_at: datetime
class ErrorResponse(BaseModel):
"""Error response format."""
error: str
details: dict = {}
# Endpoints with clear contracts
@app.route('/api/orders', methods=['POST'])
def create_order():
"""Create a new order.
Request body: CreateOrderRequest
Success (201): OrderResponse
Errors:
400: ValidationError - Invalid request data
404: NotFoundError - Customer or shipping address not found
409: ConflictError - Items out of stock
"""
try:
request_data = CreateOrderRequest(**request.json)
order = order_service.create(request_data)
return OrderResponse.from_orm(order).dict(), 201
except ValidationError as e:
return ErrorResponse(error="Validation failed",
details=e.errors()).dict(), 400
@app.route('/api/orders/<int:order_id>')
def get_order(order_id: int):
"""Get order by ID.
Path params:
order_id: Integer order ID (positive)
Success (200): OrderResponse
Errors:
404: Order not found
"""
order = order_service.get(order_id)
if not order:
return ErrorResponse(error="Order not found").dict(), 404
return OrderResponse.from_orm(order).dict()
Why this is better:
- Types are explicit: Pydantic models define exact shapes
- Validation is automatic: Invalid requests fail fast with clear errors
- Documentation is code: The models ARE the API contract
- AI agents can generate correct calls: Structure is unambiguous
Example 3: Configuration
The Problem: Configuration files that are hard to understand and easy to misconfigure.
Bad: Flat Configuration Soup
# config.yaml - What do these mean? What are valid values?
app_name: myapp
debug: true
log_level: 3
db_host: localhost
db_port: 5432
db_name: myapp_db
db_user: admin
db_pass: secret
db_pool_size: 10
db_timeout: 30
cache_enabled: yes
cache_host: localhost
cache_port: 6379
cache_ttl: 3600
cache_max_size: 1000
email_host: smtp.gmail.com
email_port: 587
email_user: app@example.com
email_pass: secret2
email_tls: 1
api_rate_limit: 100
api_timeout: 30000
feature_new_ui: true
feature_beta_api: false
Problems:
- Flat structure with no organization
- Inconsistent boolean formats (true, yes, 1)
- Unclear units (is timeout in seconds? milliseconds?)
- No validation—typos silently break things
- Related settings scattered throughout
Good: Structured Configuration
# config.yaml - Organized, typed, documented
app:
name: myapp
environment: development # development | staging | production
debug: true # Enable debug mode
logging:
level: INFO # DEBUG | INFO | WARNING | ERROR
format: json # json | text
database:
host: localhost
port: 5432
name: myapp_db
credentials:
username: admin
password: ${DB_PASSWORD} # From environment variable
pool:
size: 10 # Connection pool size
timeout_seconds: 30 # Connection timeout
cache:
enabled: true
provider: redis # redis | memcached | memory
redis:
host: localhost
port: 6379
ttl_seconds: 3600 # Default TTL for cached items
max_entries: 1000
email:
provider: smtp
smtp:
host: smtp.gmail.com
port: 587
tls: true
credentials:
username: app@example.com
password: ${EMAIL_PASSWORD}
api:
rate_limit:
requests_per_minute: 100
timeout_seconds: 30
features:
new_ui: true
beta_api: false
# config.py - Typed configuration loading
from pydantic import BaseSettings, Field
from typing import Literal
class DatabaseConfig(BaseSettings):
host: str = "localhost"
port: int = Field(default=5432, ge=1, le=65535)
name: str
username: str
password: str
pool_size: int = Field(default=10, ge=1, le=100)
timeout_seconds: int = Field(default=30, ge=1)
class CacheConfig(BaseSettings):
enabled: bool = True
provider: Literal["redis", "memcached", "memory"] = "redis"
ttl_seconds: int = Field(default=3600, ge=0)
max_entries: int = Field(default=1000, ge=1)
class AppConfig(BaseSettings):
name: str
environment: Literal["development", "staging", "production"]
debug: bool = False
database: DatabaseConfig
cache: CacheConfig
# ... other nested configs
# Load and validate on startup
config = AppConfig.parse_file("config.yaml")
Why this is better:
- Hierarchical organization: Related settings grouped together
- Clear units:
timeout_secondsvs ambiguoustimeout - Typed validation: Invalid values caught at startup
- Self-documenting: Structure shows relationships
- AI-friendly: Agent can navigate and modify specific sections
Example 4: Data Structures
The Problem: Data models with unclear meaning and relationships.
Bad: Generic Dictionaries
# What fields? What types? What's required?
def process_transaction(txn: dict) -> dict:
"""Process a transaction."""
# txn has... fields?
# Returns... data?
pass
# Actual usage reveals nothing
result = process_transaction({
"amt": 100,
"curr": "USD",
"src": "card_123",
"dst": "acct_456",
"meta": {"ip": "1.2.3.4"},
"ts": 1234567890,
"type": 1
})
# result["status"]? result["id"]? Who knows.
Problems:
- No type information at any point
- Field names are cryptic abbreviations
- No way to know required vs optional fields
- No validation—anything can be passed
Good: Explicit Data Models
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Optional
class TransactionType(Enum):
"""Type of financial transaction."""
PAYMENT = "payment"
REFUND = "refund"
TRANSFER = "transfer"
class TransactionStatus(Enum):
"""Status of a transaction."""
PENDING = "pending"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class Money:
"""Monetary amount with currency."""
amount: Decimal
currency: str # ISO 4217 code
def __post_init__(self):
if self.amount < 0:
raise ValueError("Amount cannot be negative")
if len(self.currency) != 3:
raise ValueError("Currency must be 3-letter ISO code")
@dataclass
class TransactionMetadata:
"""Optional metadata for a transaction."""
ip_address: Optional[str] = None
user_agent: Optional[str] = None
reference_id: Optional[str] = None
@dataclass
class TransactionRequest:
"""Request to process a transaction."""
transaction_type: TransactionType
amount: Money
source_account: str
destination_account: str
metadata: TransactionMetadata = field(
default_factory=TransactionMetadata
)
@dataclass
class TransactionResult:
"""Result of processing a transaction."""
transaction_id: str
status: TransactionStatus
amount: Money
created_at: datetime
error_message: Optional[str] = None
def process_transaction(
request: TransactionRequest
) -> TransactionResult:
"""Process a financial transaction.
Args:
request: Validated transaction request
Returns:
TransactionResult with status and details
Raises:
InsufficientFundsError: If source has insufficient balance
InvalidAccountError: If account IDs are invalid
"""
pass
Why this is better:
- Self-documenting: Field names explain themselves
- Type safety: Compiler/runtime catches type mismatches
- Validated: Invalid states caught at construction
- Discoverable: IDE shows available fields and types
Identifying Complexity in Your Own Code
How do you know if your interfaces need improvement? Watch for these red flags:
Red Flag 1: Documentation Dependency
Symptom: You need to read extensive docs to use the interface.
# If you need this much documentation, the interface is too complex
def configure_pipeline(
stages, # See docs Table 3.1 for stage format
options, # See docs Section 4.2 for available options
transforms, # See docs Appendix A for transform syntax
hooks # See docs Section 5.1 for hook lifecycle
):
pass
Red Flag 2: Common Usage Requires Boilerplate
Symptom: Simple operations need many lines of setup.
# If common usage looks like this, simplify it
client = ServiceClient()
client.set_auth(auth_provider.get_token())
client.configure_retry(max_retries=3, backoff=1.5)
client.set_timeout(connect=5, read=30)
client.enable_logging(level='DEBUG')
request = client.create_request('GET', '/users/123')
request.add_header('Accept', 'application/json')
response = client.execute(request)
data = response.parse_json()
# vs: data = service.get_user(123)
Red Flag 3: The Cheat Sheet
Symptom: Developers maintain notes to remember how to use it.
If your team has a wiki page titled "How to use the OrderService" with examples for common operations, the interface isn't self-explanatory.
Red Flag 4: Frequent Mistakes
Symptom: The same mistakes keep happening.
If code reviews repeatedly catch the same interface misuse, the interface is error-prone. Fix the interface, not just the usage.
Red Flag 5: The Six-Month Amnesia
Symptom: Even the author forgets how to use it.
If you can't remember how to use an interface you wrote six months ago, neither can anyone else.
Summary
These examples illustrate the core transformation: from implicit, unstructured, ambiguous interfaces to explicit, structured, clear ones. The patterns repeat:
- Group related data into typed structures
- Name things clearly so they explain themselves
- Validate at boundaries so errors are caught early
- Document contracts through types, not just comments
Whether the user is a human or an AI agent, these improvements make interfaces easier to understand and harder to misuse.