# Backend Internationalization

> Use this skill when implementing multi-language support, translation workflows, or locale-aware formatting. This skill enforces: BCP 47 locale tags, ICU MessageFormat for complex messages, dot-separated namespaced keys, fallback chains, and CI-validated translation files. Applies to any backend stack serving multi-region users. Do NOT use for: simple string key-value without plural/gender, or single-language applications.

- Skill: `j4flmao/backend-internationalization` (Agent Skill, multi-file: 9 files)
- Install (CLI): `npx skillmds@latest add j4flmao/backend-internationalization`
- Raw SKILL.md: https://api.skillmd.com/api/skills/j4flmao/backend-internationalization/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: j4flmao (https://skillmd.com/u/j4flmao)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/j4flmao/backend-internationalization

---


# Backend Internationalization

## Purpose
Design i18n architecture with locale selection, translation workflow, and message formatting.

## Agent Protocol

### Trigger
Exact user phrases: "i18n", "internationalization", "localization", "l10n", "translation", "multi-language", "locale", "language support", "translation file", "gettext", "message format", "ICU message", "pluralization", "RTL support".

### Input Context
Before activating, verify:
- Initial languages to support and priority order
- Translation workflow (in-house translators, vendor, Crowdin/Lokalise)
- Content types needing translation (API messages, emails, push notifications, error messages)
- Who provides translations (developers, product team, translation service)

### Output Artifact
Internationalization architecture design as formatted text.

### Response Format
```yaml
# Locale configuration with fallback chains
# Translation file structure
```
```typescript
// Message formatting code
// Locale detection and negotiation
```

No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.

### Completion Criteria
- [ ] Locale selection with BCP 47 tags and fallback chains
- [ ] Message storage with ICU MessageFormat and key naming convention
- [ ] Translation workflow defined (extract → translate → import → validate)
- [ ] API message delivery with Accept-Language parsing and Content-Language header
- [ ] Server-side locale-aware formatting (date, number, currency, timezone)
- [ ] RTL support with dir attribute and bidirectional text handling

### Max Response Length
200 lines of configuration and code.

## Decision Tree

### Which Library?

```
What tech stack and i18n needs?
  ├── Node.js, need ICU support, flexible
  │   └── i18next (most popular, rich ecosystem)
  ├── React, need ICU, date/number formatting
  │   └── FormatJS / react-intl (ICU native, Intl API integration)
  ├── Python / Django
  │   └── Babel + gettext (PO files, Django integration)
  ├── Python, need ICU
  │   └── Babel with babel-icu extension
  ├── Go
  │   └── go-i18n (ICU-like, YAML/TOML/JSON)
  └── Java / Spring
      └── ResourceBundle + ICU4J (standard Java i18n)
```

### How to Detect Locale?

```
Where does the user's locale come from?
  ├── Browser sends Accept-Language header
  │   └── Parse q-value, negotiate against supported list
  ├── User has saved preference in profile
  │   └── Use profile locale, ignore Accept-Language
  ├── Domain or subdomain (fr.example.com)
  │   └── Map domain to locale
  ├── GeoIP (approximate location)
  │   └── Use as default only — never override user preference
  └── Authenticated API → JWT contains locale claim
      └── Extract from token, fast-path locale resolution
```

## Workflow

### Step 1: Library Selection
| Library | Language | Message Format | Pluralization | ICU Native | Framework |
|---------|----------|---------------|---------------|------------|-----------|
| i18next | JS/TS | JSON | 6 forms | Via plugin | Universal |
| FormatJS | JS/TS | ICU | Built-in | Yes | React |
| gettext | Multi | PO/MO | 4 forms | Limited | Python/PHP |
| Fluent (Project Fluent) | Multi | FTL | Unlimited | Yes | Mozilla |
| Babel | Python | PO | 4 forms | Via babel-icu | Django |

Choose based on: ICU support requirements, framework integration, pluralization rules complexity (some languages have 6+ plural forms), and runtime performance. i18next is most popular for Node.js, FormatJS for React, gettext for Python/Django, Fluent for Mozilla projects.

### Step 2: Locale Selection and Detection
Use BCP 47 tags: `en-US`, `vi-VN`, `zh-CN`, `de-DE`, `fr-FR`, `ja-JP`, `ar-SA`. Define fallback chain per language: `es-MX` → `es-ES` → `es` → `en-US`. Store supported locales in configuration. Detect user locale from: `Accept-Language` header (q-value parsing), user profile preference (database), geolocation (CloudFront CloudFront-Viewer-Country header), domain/subdomain (`fr.example.com`). Never guess locale from IP alone — always respect user preference.

```typescript
function negotiateLocale(acceptLanguage: string): string {
  const supported = ['en-US', 'vi-VN', 'zh-CN', 'es-MX', 'es-ES', 'fr-FR', 'de-DE', 'ja-JP', 'ar-SA'];
  const fallbacks: Record<string, string> = { 'es-MX': 'es-ES', 'es-ES': 'es', 'es': 'en-US' };
  const parsed = acceptLanguage.split(',')
    .map(s => { const [tag, q = 'q=1'] = s.trim().split(';'); return { tag: tag.trim(), q: parseFloat(q.split('=')[1] || '1') }; })
    .sort((a, b) => b.q - a.q);
  for (const { tag } of parsed) {
    if (supported.includes(tag)) return tag;
    const base = tag.split('-')[0];
    if (supported.includes(base)) return base;
    if (fallbacks[tag]) return fallbacks[tag];
  }
  return 'en-US';
}
```

### Step 3: Message Storage and ICU MessageFormat
Use ICU MessageFormat for all user-facing strings — supports pluralization, gender, select, and number/date formatting. Organize translation files as JSON per locale per domain: `locales/en-US/common.json`. Key naming convention: `{domain}.{context}.{key}` — e.g., `checkout.error.card_declined`, `email.welcome.subject`. One domain per logical feature area. Shared keys in `common.json`.

```json
{
  "checkout.error.card_declined": "Your card was declined. {reason, select, insufficient_funds {Insufficient funds.} expired {Card expired.} fraud {Transaction flagged.} other {Please try another method.}}",
  "cart.item_count": "You have {count, plural, =0 {no items} one {# item} other {# items}} in your cart.",
  "invoice.total": "Total: {amount, number, ::currency/USD}",
  "email.greeting": "Hello {name}, your order {orderId} was shipped on {date, date, medium}.",
  "notification.new_follower": "{gender, select, male {He} female {She} other {They}} started following you.",
  "search.ordinal_result": "You are in {n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place."
}
```

ICU MessageFormat patterns reference: `{value, date, medium}` — date formatting. `{value, number, ::currency/USD}` — currency. `{value, number, ::percent}` — percentage. `{count, plural, =0 {none} one {# item} other {# items}}` — pluralization. `{gender, select, male {He} female {She} other {They}}` — gender selection. `{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}` — ordinal numbering.

### Step 4: Translation Workflow
Extract: CLI scans source code for `t()`/`__()` calls and outputs key catalog as JSON with context and file location. Upload: push key catalog to translation platform (Crowdin, Lokalise, POEditor) via REST API. Translate: translators work in platform UI with context strings, screenshots, and descriptions. Review: translation reviewers validate accuracy and consistency. Download: pull translated files as locale JSON. CI validates: every PR checks all keys present, no missing placeholders, ICU syntax valid. Build: bundle translation files with application or lazy-load at runtime.

```bash
# Extract keys with i18next-scanner
npx i18next-scanner --config i18next-scanner.config.js

# Push to Crowdin
crowdin upload sources --branch main

# Pull translations (after translators complete)
crowdin download --branch main --skip-untranslated-strings false
```

```typescript
// CI validation script
async function validateTranslations(): Promise<boolean> {
  const source = await loadLocale('en-US');
  const locales = ['vi-VN', 'zh-CN', 'es-MX', 'de-DE', 'fr-FR', 'ja-JP', 'ar-SA'];
  let valid = true;
  for (const locale of locales) {
    const target = await loadLocale(locale);
    for (const key of Object.keys(source)) {
      if (!target[key]) { console.error(`Missing key "${key}" in ${locale}`); valid = false; continue; }
      const sourceArgs = extractICUVars(source[key]);
      const targetArgs = extractICUVars(target[key]);
      if (JSON.stringify(sourceArgs.sort()) !== JSON.stringify(targetArgs.sort())) {
        console.error(`Argument mismatch for "${key}" in ${locale}: expected ${sourceArgs}, got ${targetArgs}`);
        valid = false;
      }
    }
  }
  return valid;
}
```

### Step 5: API Message Delivery
Parse `Accept-Language` header using quality factor (q-value). Negotiate best matching locale from supported set. Set `Content-Language` response header to the negotiated locale. Translate error messages on server side — never send raw keys to client. For emails/push: use user's stored locale preference. Cache loaded translation files in memory with LRU (max 50 locales, bound memory). For server-rendered pages, negotiate locale per request.

```typescript
// Express middleware
app.use((req, res, next) => {
  const locale = negotiateLocale(req.headers['accept-language'] || 'en-US');
  req.locale = locale;
  req.t = (key: string, params?: Record<string, unknown>) => i18next.t(key, { lng: locale, ...params });
  res.setHeader('Content-Language', locale);
  next();
});

// Error handler with translated messages
app.use((err: Error, req: Request, res: Response) => {
  const message = req.t('error.generic_server_error', { errorId: req.id });
  res.status(500).json({ error: message, errorId: req.id });
});
```

### Step 6: Locale-Aware Formatting
Use ICU for all formatting: `{value, date, medium}`, `{value, number, ::currency/USD}`, `{value, number, ::percent}`. Timezone: store UTC in DB, convert to user timezone at render time. Number formats differ: `1,234.56` vs `1.234,56`. Use `Intl.DateTimeFormat`, `Intl.NumberFormat` server-side. Never concatenate translated strings with dynamic values — always use placeholders.

| Format | en-US | de-DE | vi-VN | fr-FR | ja-JP |
|--------|-------|-------|-------|-------|-------|
| Date | Jan 15, 2025 | 15.01.2025 | 15/01/2025 | 15 janv. 2025 | 2025/01/15 |
| Time | 10:30 AM | 10:30 | 10:30 | 10:30 | 10:30 |
| Number | 1,234.56 | 1.234,56 | 1.234,56 | 1 234,56 | 1,234.56 |
| Currency | $1,234.56 | 1.234,56 € | 1.234,56 ₫ | 1 234,56 € | ¥1,235 |

### Step 7: RTL Support
RTL locales: `ar`, `ar-SA`, `he`, `he-IL`, `fa`, `fa-IR`, `ur`, `ur-PK`. Set `dir="rtl"` attribute in HTML/email response. Handle bidirectional text (BiDi) with Unicode bidi algorithm (UBA). Wrap LTR text in RTL context with Unicode characters: `\u202B` (RTL Embed), `\u202C` (Pop Directional Formatting). Use logical CSS properties: `margin-inline-start` instead of `margin-left`, `padding-inline-end` instead of `padding-right`.

### Step 8: Pluralization Rules by Language

| Language | Plural Forms | Example |
|----------|-------------|---------|
| English | 2 (singular, plural) | 1 item, 5 items |
| Russian | 4 (one, few, many, other) | 1, 2-4, 5-20, 21 |
| Arabic | 6 (zero, one, two, few, many, other) | 0, 1, 2, 3-10, 11-99, 100+ |
| Japanese | 1 (other) | All numbers use same form |
| Chinese | 1 (other) | All numbers use same form |

### Step 9: Pseudo-Localization for Testing

```typescript
// Pseudo-localization: expand strings to find layout issues
function pseudoLocalize(enText: string): string {
  return `[${enText.split('').map(c => {
    const map: Record<string, string> = { 'a': 'α', 'e': 'ε', 'o': 'σ', 'i': 'ι' };
    return map[c.toLowerCase()] || c;
  }).join('')}!!!]`;
}
```

## Lazy Loading Strategy

```typescript
const translationCache = new Map<string, Record<string, string>>();

async function loadLocale(locale: string, namespace = 'common'): Promise<Record<string, string>> {
  const key = `${locale}:${namespace}`;
  if (translationCache.has(key)) return translationCache.get(key)!;
  const response = await fetch(`/locales/${locale}/${namespace}.json`);
  const data = await response.json();
  translationCache.set(key, data);
  return data;
}
```

## ICU MessageFormat Deep Dive

```typescript
// Advanced ICU MessageFormat patterns

// Complex select with nested plurals
const complexPattern = `{items, plural,
  =0 {No items in your cart.}
  one {You have # item ({subtotal, number, ::currency/USD})}
  other {
    You have {items_count_formatted} items.
    {subtotal, number, ::currency/USD} subtotal.
    {items, plural, one {# includes a pre-order item.} other {# include pre-order items.}}
  }
}`;

// Ordinal + duration formatting
const orderStatus = `Your order is {position, selectordinal,
  one {#st}
  two {#nd}
  few {#rd}
  other {#th}
} in the queue. Estimated time: {eta, duration}`;

// Rich text formatting (i18next)
// richText_key: "Hello <bold>{{name}}</bold>, please <link>click here</link>"
// Usage:
// t('richText_key', { name: 'John', bold: (text) => `<strong>${text}</strong>`, link: (text) => `<a href="/">${text}</a>` })
```

## Translation Platform Integration

### CI/CD Translation Pipeline
```yaml
# .github/workflows/translations.yml
name: Translation Sync
on:
  push:
    branches: [main]
    paths: ['locales/en-US/**']

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Extract translation keys
        run: npx i18next-scanner --config i18next-scanner.config.js
      - name: Push source to Crowdin
        run: crowdin upload sources --branch main
      - name: Download translations
        run: crowdin download --branch main --skip-untranslated-strings false
      - name: Validate translations
        run: npx ts-node scripts/validate-translations.ts
      - name: Check coverage threshold
        run: npx ts-node scripts/check-coverage.ts --threshold 80
```

```typescript
// i18next-scanner config
// i18next-scanner.config.js
module.exports = {
  input: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}'],
  output: './locales',
  options: {
    debug: true,
    removeUnusedKeys: true,
    sort: true,
    func: {
      list: ['t', 'i18next.t', 'i18n.t'],
      extensions: ['.ts', '.tsx'],
    },
    lngs: ['en-US', 'vi-VN', 'zh-CN', 'es-MX', 'de-DE', 'fr-FR', 'ja-JP', 'ar-SA'],
    defaultLng: 'en-US',
    ns: ['common', 'checkout', 'email', 'error', 'notification'],
    defaultNs: 'common',
    resource: {
      loadPath: 'locales/{{lng}}/{{ns}}.json',
      savePath: 'locales/{{lng}}/{{ns}}.json',
    },
    context: true, // Enable context-based keys (e.g., button.save, button.cancel)
    contextFallback: true,
  },
};
```

## Locale-Specific Formatting Rules

```typescript
// Date/time formatting per locale
const dateFormats: Record<string, Intl.DateTimeFormatOptions> = {
  'en-US': { month: 'short', day: 'numeric', year: 'numeric' },           // "Jan 15, 2025"
  'de-DE': { day: '2-digit', month: '2-digit', year: 'numeric' },         // "15.01.2025"
  'vi-VN': { day: '2-digit', month: '2-digit', year: 'numeric' },         // "15/01/2025"
  'ja-JP': { year: 'numeric', month: '2-digit', day: '2-digit' },         // "2025/01/15"
  'ar-SA': { day: 'numeric', month: 'long', year: 'numeric', calendar: 'islamic' }, // "15 محرم 1446"
};

// Number formatting differences
// Number grouping: 1,234,567.89 (en) vs 1.234.567,89 (de) vs 12,34,567.89 (hi)
// Use Intl.NumberFormat with locale — never hardcode separators
function formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

// Currency formatting: handle placement and symbol variation
// $1,234.56 (en-US) vs 1.234,56 € (de-DE) vs 1,234.56 USD (en-CA)
function formatCurrency(value: number, currency: string, locale: string): string {
  return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);
}
```

## Server-Side Caching Strategy

```typescript
// Translation cache with fallback chains and TTL
class TranslationCache {
  private cache = new Map<string, Record<string, string>>();
  private ttlMs: number;
  private maxSize: number;

  constructor(ttlMs = 3600000, maxSize = 50) {
    this.ttlMs = ttlMs;
    this.maxSize = maxSize;
  }

  async getTranslation(locale: string, namespace: string): Promise<Record<string, string>> {
    const key = `${locale}:${namespace}`;
    const cached = this.cache.get(key);

    if (cached) return cached;

    // Load from file system or CDN with fallback chain
    const translations = await this.loadWithFallback(locale, namespace);

    // Evict oldest entry if at capacity
    if (this.cache.size >= this.maxSize) {
      const oldest = this.cache.keys().next().value;
      this.cache.delete(oldest);
    }

    this.cache.set(key, translations);
    setTimeout(() => this.cache.delete(key), this.ttlMs);
    return translations;
  }

  private async loadWithFallback(locale: string, namespace: string): Promise<Record<string, string>> {
    const fallbacks = [locale, locale.split('-')[0], 'en-US'];
    for (const lang of fallbacks) {
      try {
        return await loadTranslationFile(lang, namespace);
      } catch {
        continue;
      }
    }
    return {};
  }
}
```

## Configuration Reference

```yaml
i18n:
  defaultLocale: en-US
  supportedLocales: [en-US, vi-VN, zh-CN, es-MX, es-ES, de-DE, fr-FR, ja-JP, ar-SA]
  rtlLocales: [ar-SA, he-IL, fa-IR, ur-PK]
  fallbackStrategy: exact -> parent -> default -> key
  namespaces: [common, checkout, email, error, notification]
  cache:
    type: lru
    maxSize: 50
    ttlMs: 3600000
  lazyLoad: true
  pseudoLocalization: false
  ci:
    validatePlaceholders: true
    validateSyntax: true
    requireAllKeys: true
```

## Production Considerations

| Concern | Practice |
|---------|----------|
| Translation file size | Split by namespace. Namespace per feature. Lazy-load on first use |
| Missing translations | Fallback chain: exact → parent → default → key name returned |
| Performance | Cache translations in-memory. LRU with TTL |
| CDN for translation files | Serve locale JSON from CDN with versioned URLs |
| Translation coverage | CI fails if coverage < 80% of source keys |
| Context for translators | Every key has description, max length hint, and screenshot |
| Dynamic content (user names, dates) | Always use ICU placeholders — never concatenate translated text with values |
| RTL layout | Flip layout mirrors, icons, and alignment in addition to text direction |
| Number of supported locales | Each locale adds maintenance cost. Only add when business need exists |
| Translation memory reuse | Reuse identical translations across keys to save translator cost |

## Security

| Risk | Mitigation |
|------|-----------|
| Injection via translation | ICU MessageFormat supports code execution in some parsers — always validate/sanitize |
| Missing placeholders | CI validates argument matching between source and target locales |
| RTL override injection | Sanitize user-generated content in RTL contexts (Unicode bidi overrides) |
| Excessive locale loading | Rate-limit locale file requests, cache aggressively |
| Translator access to source code | Use translation platform with proper RBAC — translators should not need repo access |
| Exposed translation keys in API | Return translated messages, never raw keys, in API responses |

## Anti-Patterns

| Anti-Pattern | Why It's Bad | Fix |
|-------------|-------------|-----|
| English-only keys as fallback (e.g., `checkout.error.card_declined` returns `"checkout.error.card_declined"`) | Confusing for users | Fallback to default locale (en-US) translation, not the key |
| String concatenation | Breaks in RTL, wrong word order | Use placeholders: `Hello {name}` not `"Hello " + name` |
| Large monolithic translation files | Slow to load, hard to maintain | Split by namespace per feature |
| No ICU for plurals | Wrong grammar in many languages | Use ICU plural syntax |
| Client-side only i18n | SEO fails, server errors not translated | Translate on server for SSR and API errors |
| Inline strings mixed with code | Impossible to extract for translators | All strings in translation files, none in code |
| Machine-only translations without review | Low quality, cultural insensitivity | Always have human review machine translations before release |
| Over-translation of technical terms | Brand names, product names, and commands should stay untranslated | Use ICU `select` or `=0` syntax for untranslated terms |

## Rules
- Locale = BCP 47 tag. Never language-only (`en` not `english`)
- Keys are dot-separated and namespaced
- ICU MessageFormat for all user-facing strings
- Fallback chain: exact locale → parent locale → default locale → key itself
- Translation files are version-controlled and CI-validated
- Every string has a description for translators
- No concatenation of translated strings — use placeholders
- Store timestamps in UTC, format at render time
- RTL languages require `dir="rtl"` attribute and BiDi handling
- Never translate error messages at client — always translate server-side
- Extract translation strings as CI step, never manually maintain key lists

## References
  - references/i18n-architecture.md — Internationalization Architecture
  - references/i18n-frontend.md — Frontend Internationalization
  - references/i18n-libraries.md — i18n Libraries
  - references/i18n-performance.md — i18n Performance
  - references/i18n-testing.md — i18n Testing
  - references/l10n-patterns.md — L10n Patterns
  - references/rtl-i18n.md — RTL and Complex Scripts Reference
  - references/translation-workflow.md — Translation Workflow Reference
## Handoff
`frontend-universal/animation` for RTL UI considerations and animation direction
## Implementation Patterns

### Observer Pattern for Event Handling
`
interface EventObserver<T> {
  onEvent(event: T): Promise<void>;
}

class EventBus<T> {
  private observers: Set<EventObserver<T>> = new Set();
  subscribe(observer: EventObserver<T>): void {
    this.observers.add(observer);
  }
  unsubscribe(observer: EventObserver<T>): void {
    this.observers.delete(observer);
  }
  async emit(event: T): Promise<void> {
    const results = Array.from(this.observers).map(o => o.onEvent(event));
    await Promise.allSettled(results);
  }
}
`

### Configuration-Driven Approach
`
config:
  defaults:
    timeout: 30s
    retryCount: 3
  overrides:
    production:
      timeout: 60s
      retryCount: 5
    development:
      timeout: 300s
      retryCount: 1
`

## Production Considerations

### Deployment Checklist
- [ ] Configuration validated against schema before startup
- [ ] Health check endpoints registered and monitored
- [ ] Graceful shutdown with draining period (30s timeout)
- [ ] Resource limits configured (CPU, memory, file descriptors)
- [ ] Log level set appropriate for environment
- [ ] Metrics endpoint secured and exposed
- [ ] Rate limiting configured per-tier
- [ ] TLS certificates valid and auto-renewing
- [ ] Database migrations run as separate deployment step
- [ ] Feature flags ready for gradual rollout

### Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|--------|-----------|----------|--------|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |

## Anti-Patterns

| Anti-Pattern | Symptom | Root Cause | Solution |
|-------------|---------|------------|----------|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |

## Performance Optimization

### Caching Strategy
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge).
Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).

### Resource Pooling
- Database connections: Pool of reusable connections (HikariCP, pgBouncer)
- HTTP connections: Keep-alive + connection pooling for external calls
- Thread pool: Bounded thread pools for async task execution

### Profiling Methodology
1. Establish baseline with production traffic profile
2. Profile CPU with sampling profiler (pprof, perf, async-profiler)
3. Profile memory with heap dumps and allocation tracking
4. Profile I/O with strace/perf trace for syscall analysis
5. Profile latency with distributed tracing (OpenTelemetry)
6. Identify bottleneck, formulate hypothesis, implement fix
7. Re-profile to verify improvement, repeat

## Security Considerations

### Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege

### Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification

### Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets

## Rules
- Default-deny security posture — allow only explicitly required access.
- All inputs validated, all outputs encoded, all errors handled.
- Defend in depth — multiple layers of security controls.
- Fail securely — errors default to safe behavior.
- Log security-relevant events for audit and investigation.
- Keep dependencies updated — automate vulnerability scanning.
- Design for observability from day one, not as an afterthought.
- Document all architectural decisions with rationale.
- Review code for security, performance, and correctness before merging.
