architecture-decision-records

Architecture Decision Records (ADR) - Writing Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "architecture-decision-records" with this command: npx skills add tejovanthn/rasikalife/tejovanthn-rasikalife-architecture-decision-records

Architecture Decision Records (ADR) - Writing Guide

This skill provides comprehensive guidance for writing high-quality Architecture Decision Records (ADRs) for the VedaGhosham project, based on the existing 13 ADRs in docs/architecture-decisions/ .

What is an ADR?

An Architecture Decision Record documents a significant technical decision made during the project's development. ADRs capture:

  • What decision was made

  • Why it was made (problem context)

  • How it's implemented (with code examples)

  • Trade-offs (what was gained/lost)

  • Alternatives considered

When to Write an ADR

Write an ADR when making decisions that:

  • Impact the entire system (database choice, framework selection, deployment strategy)

  • Are difficult to reverse (DynamoDB single-table design, monorepo structure)

  • Involve significant trade-offs (tRPC vs REST, Remix vs Next.js)

  • Set architectural patterns (entity structure, error handling, authentication)

  • Require future context (why we chose this approach over alternatives)

Examples of ADR-Worthy Decisions

  • ✅ Choosing DynamoDB single-table design (ADR-004)

  • ✅ Selecting tRPC over REST (ADR-005)

  • ✅ Email blacklist management strategy (ADR-015)

  • ✅ Fresh user data from database vs JWT (ADR-001)

  • ✅ Monorepo with pnpm workspaces (ADR-009)

Examples of Non-ADR Decisions

  • ❌ Adding a new field to an entity (routine change)

  • ❌ Refactoring a component (implementation detail)

  • ❌ Fixing a bug (not an architectural decision)

  • ❌ Updating dependencies (maintenance task)

ADR Format and Structure

File Naming Convention

docs/architecture-decisions/adr-NNN-kebab-case-title.md

  • NNN: Zero-padded 3-digit number (001, 002, 015, etc.)

  • kebab-case-title: Descriptive, lowercase, hyphen-separated

  • Find the next number by checking existing ADRs

Examples:

  • adr-001-fresh-user-data.md

  • adr-005-trpc-over-rest.md

  • adr-015-email-blacklist-bounce-management.md

Document Structure

Every ADR follows this structure:

ADR-NNN: Title (Clear, Concise Description)

Date: YYYY-MM-DD Status: Accepted | Proposed | Deprecated | Superseded Context: One-line summary of when/why this decision was needed


Problem

[Detailed problem description with 3-5 paragraphs]

Option 1: [First Alternative]

[Code example or description]

Problems:

  • List specific issues
  • Technical limitations
  • Business concerns

Option 2: [Second Alternative]

[Code example or description]

Problems:

  • List specific issues

Option 3: [Chosen Approach]

[Code example or description]

Benefits:

  • Why this is better
  • Advantages over alternatives

Decision

Use [Chosen Approach] for [Purpose].

[1-2 paragraph explanation of the decision]

Why [Chosen Approach]:

  1. Reason 1
  2. Reason 2
  3. Reason 3

Implementation

[Detailed implementation with code examples]

1. [First Implementation Aspect]

[Code examples with comments]

2. [Second Implementation Aspect]

[Code examples with comments]

[Continue for all major implementation aspects]


Benefits

1. [Benefit Name]

[Explanation with code/metrics examples]

2. [Benefit Name]

[Explanation with code/metrics examples]

[Continue for all major benefits]


Trade-offs

What We Gained

  • ✅ Benefit 1
  • ✅ Benefit 2
  • ✅ Benefit 3

What We Lost

  • ❌ Limitation 1
  • ❌ Limitation 2
  • ❌ Limitation 3

Why Trade-offs Are Acceptable

  1. Limitation 1: Explanation of why it's acceptable
  2. Limitation 2: Explanation
  3. Limitation 3: Explanation

Comparison

FeatureAlternative 1Alternative 2Chosen Approach
Metric 1ValueValueValue
Metric 2ValueValueValue

Real-World Example

Scenario: [Concrete Use Case]

With [Alternative Approach]:

[Step-by-step flow showing problems]

With [Chosen Approach]:

[Step-by-step flow showing benefits]

Winner: [Chosen Approach] ([quantified benefit])


Future Considerations

[Future Topic 1]

[Discussion of how this decision might evolve]

[Future Topic 2]

[Potential extensions or modifications]

Current Status: [Current state and future plans]


Related Files

[Category 1]

  • path/to/file1.ts - Description
  • path/to/file2.ts - Description

[Category 2]

  • path/to/file3.ts - Description

References

  • [Resource Name]: [URL]
  • [Documentation]: [URL]
  • Related: [Cross-reference to other ADRs]

Writing Guidelines

  1. Title and Header

Title Format:

ADR-NNN: Clear, Concise Description

Examples:

  • ✅ ADR-001: Load Fresh User Data from Database (Not Stale JWT)

  • ✅ ADR-005: tRPC Over REST API

  • ✅ ADR-015: Email Blacklist and Bounce Management

  • ❌ ADR-001: User Data (too vague)

  • ❌ ADR-005: API Decision (not descriptive)

Header Fields:

Date: 2025-12-17 (ISO format) Status: Accepted (current state) Context: Brief one-liner about when/why this decision was needed

Status Values:

  • Proposed

  • Under consideration

  • Accepted

  • Active and implemented

  • Deprecated

  • No longer recommended

  • Superseded

  • Replaced by another ADR (reference ADR-XXX)

  1. Problem Section

The Problem section should:

  • Explain the context (3-5 paragraphs minimum)

  • Show code examples of the problem or alternatives

  • List 2-4 alternatives considered

  • Explain trade-offs for each alternative

Good Problem Description Example:

Problem

VedaGhosham sends authentication OTP emails via AWS SES. Without proper bounce and complaint handling, we risk:

  1. Sender Reputation Damage: Repeated bounces to invalid emails harm sender reputation
  2. Service Suspension: AWS SES may suspend service if bounce rate exceeds 5%
  3. Wasted Resources: Sending emails to blacklisted addresses wastes API calls and costs
  4. Poor User Experience: Users with invalid emails get stuck in auth flow
  5. No Visibility: No tracking of email delivery issues or spam complaints

Without Blacklist Management

// ❌ Sends to all emails without checking
export async function sendOTPEmail(email: string, code: string) {
  // Always sends, even if email previously bounced
  await sesClient.send(new SendEmailCommand({ ... }));
}

Problems:

- Hard bounces (permanent failures) get retried every login

- Spam complaints not tracked (user marked email as spam)

- No way to prevent sending to known-bad addresses

- AWS SES reputation degrades over time

**Key Points:**
- Start with business impact (reputation, cost, UX)
- Show concrete code examples
- List specific, actionable problems

### 3. Decision Section

**Format:**
```markdown
## Decision

**Use [Technology/Pattern] for [Purpose].**

[1-2 paragraph justification]

**Why [Chosen Approach]:**
1. Reason 1 (with evidence)
2. Reason 2 (with evidence)
3. Reason 3 (with evidence)

Example:

## Decision

**Use tRPC (TypeScript Remote Procedure Call) instead of REST or GraphQL.**

tRPC provides end-to-end type safety without codegen, perfect for our TypeScript monorepo.

**Why tRPC:**
1. **Full type safety with zero codegen**: Types flow automatically through AppRouter export
2. **Automatic validation**: Zod schemas validate input before handler runs
3. **Middleware composition**: Reusable auth/permission middleware
4. **Request batching**: Reduces Lambda invocations and latency

4. Implementation Section

The Implementation section should:

- Show working code examples (not pseudocode)

- Include file paths in code comments

- Explain key patterns with inline comments

- Cover all major aspects of implementation

Structure:

## Implementation

### 1. [Infrastructure/Configuration]
[SST/config code with explanations]

### 2. [Core Business Logic]
[Entity/service code]

### 3. [API Layer]
[tRPC router/endpoint code]

### 4. [Frontend Integration]
[Remix route/component code]

### 5. [Testing/Monitoring]
[Test examples, CloudWatch alarms]

Code Example Standards:

// ✅ Good: Working code with comments
// packages/core/src/email/blacklist.ts
export async function checkEmailBlacklist(
  email: string,
): Promise<BlacklistCheckResult> {
  const normalizedEmail = normalizeEmail(email);

  try {
    const result = await User.query.byEmail({ email: normalizedEmail }).go();

    if (result.data.length > 0) {
      const user = result.data[0];

      if (user.emailBlacklisted) {
        return {
          isBlacklisted: true,
          reason: user.emailBlacklistReason || 'UNKNOWN',
          userId: user.userId,
          blacklistedAt: user.emailBlacklistedAt,
        };
      }
    }

    return { isBlacklisted: false };
  } catch (error) {
    // Database error - fail open to prevent auth system breakage
    console.error('Failed to check email blacklist, allowing send:', error);
    return { isBlacklisted: false };
  }
}

// ❌ Bad: Pseudocode without context
function checkBlacklist(email) {
  // Check if email is blacklisted
  return query(email);
}

5. Benefits Section

Format:

## Benefits

### 1. **[Concrete Benefit]**
[Explanation with evidence]

**Example/Metric:**
[Code example or quantified metric]

### 2. **[Concrete Benefit]**
[Explanation with evidence]

Good Benefits Example:

## Benefits

### 1. **Cost Efficiency**
Single table = one set of provisioned capacity. No cross-table query overhead.

**Example:**
- Multi-table: 7 tables × $5/month = **$35/month** (minimum)
- Single-table: 1 table × $5/month = **$5/month**
- **Savings: 85%** (at small scale)

### 2. **Consistent Performance**
All queries within one table (no cross-table latency). GSIs provide O(1) lookups 
for all access patterns. Single-digit millisecond latency.

Key Points:

- Use bold for benefit names

- Provide concrete evidence (metrics, code examples)

- Quantify when possible (percentages, time, cost)

6. Trade-offs Section

Format:

## Trade-offs

### What We Gained
- ✅ Benefit 1
- ✅ Benefit 2
- ✅ Benefit 3

### What We Lost
- ❌ Limitation 1
- ❌ Limitation 2
- ❌ Limitation 3

### Why Trade-offs Are Acceptable

1. **Limitation 1**: Detailed explanation of why this is acceptable
2. **Limitation 2**: Explanation with context
3. **Limitation 3**: Explanation

Good Trade-offs Example:

## Trade-offs

### What We Gained
- ✅ Full type safety with zero codegen
- ✅ Automatic validation (Zod schemas)
- ✅ Simplified error handling (TRPCError)
- ✅ Middleware composition for auth/permissions
- ✅ Request batching (reduces Lambda invocations)
- ✅ IntelliSense for all procedures

### What We Lost
- ❌ No OpenAPI/Swagger documentation (tRPC-specific)
- ❌ Smaller ecosystem than REST (fewer tools/integrations)
- ❌ TypeScript-only (can't call from Python, Go, etc.)
- ❌ Less familiar to REST-only developers

### Why Trade-offs Are Acceptable

1. **No OpenAPI**: Internal API only (not public-facing); tRPC docs are code itself
2. **Smaller Ecosystem**: TypeScript ecosystem is sufficient for our needs
3. **TypeScript-Only**: Entire stack is TypeScript (no other languages planned)
4. **Learning Curve**: One-time cost; tRPC is simpler than REST + OpenAPI codegen

7. Comparison Section

Use tables to compare alternatives across multiple dimensions:

## Comparison

| Feature | Alternative 1 | Alternative 2 | Chosen Approach |
|---------|--------------|---------------|-----------------|
| **Feature 1** | ❌ No | ⚠️ Partial | ✅ Yes |
| **Feature 2** | Value | Value | Value |
| **Cost** | $X/month | $Y/month | $Z/month |
| **Performance** | Metric | Metric | Metric |

Symbols to Use:

- ✅ = Advantage

- ❌ = Disadvantage

- ⚠️ = Partial/Conditional

- Use actual values when possible (numbers, percentages)

8. Real-World Example

Provide a concrete scenario showing the decision in action:

## Real-World Example

### Scenario: Add "dateOfBirth" field to User

**With Polyrepo (Multi-Repository):**
```bash
# 1. Update core
cd vedaghosham-core
# Edit User entity
git commit -m "Add dateOfBirth"
npm version patch      # 1.2.0 → 1.2.1
npm publish

# 2. Update API
cd ../vedaghosham-api
npm install @vedaghosham/core@1.2.1  # Wait for npm registry propagation
# Update API code
git commit -m "Support dateOfBirth"

# 3. Update Web
cd ../vedaghosham-web
npm install @vedaghosham/core@1.2.1
# Update web code
git commit -m "Show dateOfBirth in profile"

# Total time: 30-60 minutes

With Monorepo (Chosen Approach):

# 1. Update core
cd packages/core/src/user
# Edit entity.ts and types.ts

# 2. Update functions (TypeScript error guides you!)
cd packages/functions/src/user
# Update router.ts

# 3. Update web (TypeScript error guides you!)
cd packages/web/app/routes
# Update profile route

# 4. Commit everything
git commit -m "Add dateOfBirth field"

# Total time: 5-10 minutes ✅

Winner: Monorepo (6× faster, no versioning hassle)

### 9. Future Considerations

Discuss how the decision might evolve:

```markdown
## Future Considerations

### Adding Public API
If we need a public API (for mobile apps, third-party integrations):
- **Option A**: Keep tRPC for internal use; add separate REST API for public
- **Option B**: Use tRPC + tRPC-OpenAPI adapter (generates OpenAPI from tRPC)

**Current Status:** Internal API only; no public API needed yet.

### Non-TypeScript Clients
If we need Python/Go clients:
- **Option A**: Create thin REST wrapper around tRPC backend
- **Option B**: Use HTTP directly (tRPC is just JSON-RPC over HTTP)

**Current Status:** Full TypeScript stack; no other languages planned.

10. Related Files

List all files implementing the decision:

## Related Files

### Backend
- `packages/functions/src/shared/trpc.ts` - tRPC initialization
- `packages/functions/src/shared/middleware.ts` - Auth middleware
- `packages/functions/src/{entity}/router.ts` - Entity routers

### Frontend
- `packages/web/app/lib/trpc.ts` - tRPC client factory
- `packages/web/app/routes/_app.*.tsx` - Usage in Remix routes

### Schemas
- `packages/core/src/{entity}/schema.ts` - Zod schemas used by tRPC

11. References

Include all relevant resources:

## References

- **tRPC Documentation**: https://trpc.io/
- **tRPC vs REST**: https://trpc.io/docs/concepts
- **ElectricSQL tRPC Talk**: https://www.youtube.com/watch?v=2LYM8gf184U
- **Zod Validation**: https://zod.dev/
- Related: ADR-004 (Single-Table DynamoDB)
- Related: ADR-010 (Zod Shared Validation)

Code Example Standards

1. Use Real, Working Code

// ✅ Good: Actual code from codebase
export async function loader({ request }: LoaderFunctionArgs) {
  await requireAuth(request);
  const trpc = await createServerClient(request);
  const user = await trpc.user.me.query();
  return json({ user });
}

// ❌ Bad: Pseudocode
export function loader() {
  // Get user
  return user;
}

2. Include File Paths

// ✅ Good: Shows where code lives
// packages/web/app/routes/_app.tsx
export async function loader({ request }: LoaderFunctionArgs) {
  // ...
}

// ❌ Bad: No context
export async function loader() {
  // ...
}

3. Add Explanatory Comments

// ✅ Good: Explains why
try {
  const result = await User.query.byEmail({ email: normalizedEmail }).go();
  return { isBlacklisted: false };
} catch (error) {
  // Database error - fail open to prevent auth system breakage
  console.error('Failed to check email blacklist, allowing send:', error);
  return { isBlacklisted: false };
}

// ❌ Bad: No explanation for non-obvious behavior
} catch (error) {
  return { isBlacklisted: false };
}

4. Show Before/After

// ❌ WRONG: Checks stale JWT token
export async function loader({ request }: LoaderFunctionArgs) {
  const userData = await requireCompleteProfile(request);
  // userData.profileCompleted is STALE!
}

// ✅ CORRECT: Loads fresh data from database
export async function loader({ request }: LoaderFunctionArgs) {
  await requireAuth(request);
  const trpc = await createServerClient(request);
  const user = await trpc.user.me.query();
  // user.profileCompleted is always fresh! ✅
}

Writing Style Guidelines

1. Be Concrete, Not Abstract

- ✅ "Single table costs $5/month vs multi-table $35/month (85% savings)"

- ❌ "Single table is cheaper"

2. Use Numbers and Metrics

- ✅ "3-6× faster initial load (300-500ms vs 2-3 seconds)"

- ❌ "Much faster initial load"

3. Show, Don't Just Tell

- ✅ Include code examples showing the problem and solution

- ❌ "The old approach had issues"

4. Explain Trade-offs Honestly

- ✅ "What we lost: No OpenAPI docs. Why acceptable: Internal API only"

- ❌ "Perfect solution with no downsides"

5. Future-Proof with Considerations

- ✅ "If we need public API: Option A vs Option B. Current status: Not needed"

- ❌ "We'll never need a public API"

Common Pitfalls to Avoid

❌ Don't Write Vague ADRs

## Decision
We decided to use a database.

❌ Don't Skip Alternatives

## Problem
We need to store data.

## Decision
Use DynamoDB.

(Missing: Why not PostgreSQL? MongoDB? What were the trade-offs?)

❌ Don't Use Pseudocode

function doSomething() {
  // Do the thing
}

❌ Don't Ignore Trade-offs

## Benefits
- Fast
- Cheap
- Easy

(Missing: What did we lose? Every decision has trade-offs)

❌ Don't Skip Real Examples

## Implementation
We implemented the feature.

(Missing: How? Show code!)

Checklist for Reviewing ADRs

Before finalizing an ADR, ensure:

-  Title is clear and descriptive (ADR-NNN format)

-  Date is in YYYY-MM-DD format

-  Status is set (Proposed/Accepted/Deprecated/Superseded)

-  Problem section explains context with 3+ paragraphs

-  Alternatives section lists 2-4 options considered

-  Decision section has clear justification (3+ reasons)

-  Implementation has real code examples with file paths

-  Benefits section quantifies advantages (metrics/percentages)

-  Trade-offs section honestly discusses limitations

-  Comparison table compares alternatives across multiple dimensions

-  Real-World Example shows concrete scenario

-  Future Considerations discusses evolution/extensions

-  Related Files lists all implementation files

-  References includes external docs and cross-references

-  Code examples use ✅/❌ to show right/wrong patterns

-  Numbers and metrics used throughout (not just "better/faster")

-  All code examples include comments explaining why

ADR Template

Use this template when creating a new ADR:

# ADR-NNN: [Title]

**Date:** YYYY-MM-DD
**Status:** Proposed | Accepted | Deprecated | Superseded
**Context:** [One-line summary]

---

## Problem

[3-5 paragraph problem description]

### Option 1: [First Alternative]
[Code/description]

**Problems:**
- Issue 1
- Issue 2

### Option 2: [Second Alternative]
[Code/description]

**Problems:**
- Issue 1
- Issue 2

---

## Decision

**Use [Chosen Approach] for [Purpose].**

[1-2 paragraph justification]

**Why [Chosen Approach]:**
1. Reason 1
2. Reason 2
3. Reason 3

---

## Implementation

### 1. [Implementation Aspect 1]
[Code examples]

### 2. [Implementation Aspect 2]
[Code examples]

---

## Benefits

### 1. **[Benefit Name]**
[Explanation with evidence]

### 2. **[Benefit Name]**
[Explanation with evidence]

---

## Trade-offs

### What We Gained
- ✅ Benefit 1
- ✅ Benefit 2

### What We Lost
- ❌ Limitation 1
- ❌ Limitation 2

### Why Trade-offs Are Acceptable
1. **Limitation 1**: Explanation
2. **Limitation 2**: Explanation

---

## Comparison

| Feature | Alternative 1 | Alternative 2 | Chosen |
|---------|--------------|---------------|--------|
| **Feature 1** | Value | Value | Value |

---

## Real-World Example

### Scenario: [Use Case]

**With [Alternative]:**
[Step-by-step flow]

**With [Chosen Approach]:**
[Step-by-step flow]

**Winner:** [Chosen] ([quantified benefit])

---

## Future Considerations

### [Topic 1]
[Discussion]

**Current Status:** [State]

---

## Related Files

### [Category 1]
- `path/to/file.ts` - Description

---

## References

- **[Resource]**: [URL]
- Related: ADR-XXX

Example: Simplified ADR

Here's a simplified example showing the key elements:

# ADR-016: Rate Limiting for API Endpoints

**Date:** 2025-12-20
**Status:** Proposed
**Context:** Protecting API from abuse and ensuring fair usage

---

## Problem

VedaGhosham's tRPC API is publicly accessible (after authentication). Without 
rate limiting, we risk:

1. **DoS Attacks**: Malicious users could overwhelm Lambda functions
2. **Cost Spikes**: Unlimited requests = unlimited AWS costs
3. **Poor UX**: One user's heavy usage slows down others
4. **No Abuse Protection**: No mechanism to block bad actors

### Option 1: No Rate Limiting
```typescript
// No protection - anyone can spam requests
export const userRouter = router({
  list: protectedProcedure.query(async () => {
    return await User.scan.go(); // Expensive query, no limits
  }),
});

Problems:

- No protection against abuse

- AWS costs unbounded

- Poor experience for legitimate users

Option 2: Lambda Concurrency Limits

Set reserved concurrency on Lambda functions.

Problems:

- All-or-nothing (affects all users equally)

- No per-user fairness

- Hard to configure correctly

Option 3: DynamoDB-Based Rate Limiting

Track request counts per user in DynamoDB with TTL.

Benefits:

- Per-user fairness

- Serverless (no Redis needed)

- Fine-grained control

Decision

Use DynamoDB-based rate limiting with per-user counters and TTL.

We'll create a RateLimit entity tracking requests per user per time window.

Why DynamoDB Rate Limiting:

- Serverless: No Redis/Memcached infrastructure needed

- Per-User Fairness: Each user has independent limit

- Automatic Cleanup: DynamoDB TTL removes old records

- Fine-Grained: Can set limits per endpoint or globally

Implementation

1. RateLimit Entity

// packages/core/src/rate-limit/entity.ts
export const RateLimit = new Entity({
  model: {
    entity: 'RateLimit',
    version: '1',
    service: 'vedaghosham',
  },
  attributes: {
    userId: { type: 'string', required: true },
    endpoint: { type: 'string', required: true },
    windowStart: { type: 'number', required: true }, // Unix timestamp
    requestCount: { type: 'number', required: true, default: 0 },
    ttl: { type: 'number' }, // DynamoDB TTL (auto-delete after 1 hour)
  },
  indexes: {
    primary: {
      pk: { field: 'pk', composite: ['userId', 'endpoint'] },
      sk: { field: 'sk', composite: ['windowStart'] },
    },
  },
}, configuration);

2. Rate Limiting Middleware

// packages/functions/src/shared/rate-limit.ts
const RATE_LIMITS = {
  'user.list': { requests: 100, windowMs: 60000 }, // 100 req/min
  'course.create': { requests: 10, windowMs: 60000 }, // 10 req/min
  default: { requests: 1000, windowMs: 60000 }, // 1000 req/min
};

export const rateLimitMiddleware = t.middleware(async ({ ctx, path, next }) => {
  const userId = ctx.user?.userId;
  if (!userId) return next(); // Skip for unauthenticated

  const limit = RATE_LIMITS[path] || RATE_LIMITS.default;
  const windowStart = Math.floor(Date.now() / limit.windowMs) * limit.windowMs;

  // Check current count
  const result = await RateLimit.get({
    userId,
    endpoint: path,
    windowStart,
  }).go();

  if (result.data && result.data.requestCount >= limit.requests) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: `Rate limit exceeded. Try again in ${limit.windowMs / 1000}s`,
    });
  }

  // Increment counter
  await RateLimit.upsert({
    userId,
    endpoint: path,
    windowStart,
    requestCount: (result.data?.requestCount || 0) + 1,
    ttl: Math.floor(Date.now() / 1000) + 3600, // Expire after 1 hour
  }).go();

  return next();
});

3. Apply to Procedures

// packages/functions/src/shared/procedures.ts
export const protectedProcedure = t.procedure
  .use(isAuthenticated)
  .use(rateLimitMiddleware);

Benefits

1. Cost Protection

Rate limiting prevents runaway costs from abuse.

Example:

- Without limit: Malicious user makes 1M requests = $200+ Lambda cost

- With limit: Blocked at 1,000 requests = $0.20 maximum cost per user

- Savings: 99.9% in abuse scenario

2. Fair Resource Allocation

Each user has independent quota (one user can't starve others).

3. No Additional Infrastructure

Uses existing DynamoDB table (no Redis/Memcached needed).

Trade-offs

What We Gained

- ✅ Protection from abuse and DoS

- ✅ Cost predictability

- ✅ Per-user fairness

- ✅ Serverless (no Redis)

What We Lost

- ❌ DynamoDB write costs (1 write per request)

- ❌ Slight latency increase (~10ms per request)

- ❌ Complexity (middleware + entity)

Why Trade-offs Are Acceptable

- DynamoDB Writes: ~$0.001 per 1,000 requests (negligible vs abuse cost)

- Latency: 10ms overhead acceptable for cost/abuse protection

- Complexity: Essential for production API

Comparison

Feature
No Limit
Lambda Limits
DynamoDB Limit

Cost Protection
❌ No
⚠️ Global
✅ Per-user

Fairness
❌ No
❌ No
✅ Yes

Infrastructure
✅ None
✅ None
✅ Existing DB

Granularity
N/A
❌ Coarse
✅ Fine-grained

Real-World Example

Scenario: User spams "list all courses" endpoint

Without Rate Limiting:

User makes 10,000 requests in 1 minute
→ 10,000 Lambda invocations × $0.0000002 = $2.00
→ 10,000 DynamoDB scans × $0.00025 = $2.50
→ Total cost: $4.50 per abuse incident
→ API slow for all users

With Rate Limiting (100 req/min):

User makes 10,000 requests in 1 minute
→ First 100 succeed
→ Next 9,900 blocked with 429 error
→ Cost: 100 requests = $0.045
→ Other users unaffected

Winner: Rate limiting (99% cost reduction, better UX)

Future Considerations

Distributed Rate Limiting

If we need multi-region:

- Use DynamoDB Global Tables for consistent limits across regions

Current Status: Single region sufficient.

Redis Alternative

If DynamoDB becomes too expensive:

- Switch to ElastiCache Redis (faster, lower per-request cost)

Current Status: DynamoDB cost acceptable (<$1/month).

Related Files

Core

- packages/core/src/rate-limit/entity.ts
 - RateLimit entity

Functions

- packages/functions/src/shared/rate-limit.ts
 - Middleware

- packages/functions/src/shared/procedures.ts
 - Applied to procedures

Infrastructure

- infra/database.ts
 - DynamoDB table (already has TTL enabled)

References

- DynamoDB TTL: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html

- Rate Limiting Patterns: https://aws.amazon.com/blogs/architecture/rate-limiting-strategies-for-serverless-applications/

- tRPC Middleware: https://trpc.io/docs/server/middlewares

- Related: ADR-004 (Single-Table DynamoDB)

- Related: ADR-005 (tRPC Over REST)

---

## Summary

High-quality ADRs in VedaGhosham:
1. **Document significant decisions** (not routine changes)
2. **Show alternatives considered** (2-4 options with trade-offs)
3. **Provide working code examples** (real code from codebase)
4. **Quantify benefits** (metrics, percentages, cost savings)
5. **Explain trade-offs honestly** (what we lost and why it's acceptable)
6. **Include comparison tables** (features across alternatives)
7. **Show real-world scenarios** (concrete before/after examples)
8. **Discuss future evolution** (how decision might change)
9. **List all related files** (where implementation lives)
10. **Reference external docs** (AWS docs, library docs, related ADRs)

Use the ADR template and checklist to ensure comprehensive, maintainable architecture documentation that will serve the project for years to come.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-templates

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review
General

conform

No summary provided by upstream source.

Repository SourceNeeds Review