Platform Resiliency & Fault Tolerance
How Grade My Investments stays reliable through retry logic, graceful degradation, caching, and self-healing infrastructure.
Resiliency Philosophy
Grade My Investments is built on the principle that failures are inevitable — outages are not. Every external dependency (APIs, databases, payment processors, email services) can and will fail transiently. The platform is designed to absorb these failures transparently through layered retry logic, caching, graceful degradation, and self-healing services.
flowchart LR
EXT([External Request<br/>FMP - Stripe - Email])
RET{Transient<br/>Error?}
BO[Exponential Backoff<br/>3 retries - 1s-2s-4s]
CACHE[(FMP Cache<br/>5-min in-memory)]
DB[(MySQL<br/>EF Retry x3)]
SVC[11 Background Services<br/>systemd Restart=always]
IDEM{Already<br/>processed?}
SKIP[Skip - idempotent]
DONE[Operation<br/>Complete]
GD[Graceful Degradation<br/>partial result returned]
EXT --> RET
RET -- yes --> BO --> RET
RET -- no - client error --> GD
RET -- success --> CACHE
CACHE --> DB
DB --> SVC
SVC --> IDEM
IDEM -- yes --> SKIP
IDEM -- no --> DONE
classDef app fill:#1f2630,stroke:#C0894E,color:#F3EFE8;
classDef svc fill:#241c14,stroke:#E2B583,color:#F3EFE8;
classDef data fill:#22201a,stroke:#C0894E,color:#EBD3AE;
classDef ext fill:#1b1f27,stroke:#5A616B,color:#C4C0B8;
classDef sec fill:#1d2733,stroke:#5cc8e0,color:#eafaff;
class EXT ext
class RET,IDEM app
class BO,GD svc
class CACHE,DB data
class SVC,DONE,SKIP app
1. Exponential Backoff Retry
All external API calls use exponential backoff retry with intelligent error classification. Only transient failures trigger retries — client errors (400, 401, 403, 404) fail immediately since retrying won't help.
FMP Financial Data API
| Parameter | Value |
|---|---|
| Max retries | 3 |
| Backoff schedule | 1s → 2s → 4s |
| Retry on | 429 (rate limit), 5xx (server errors) |
| Don't retry on | 400, 401, 403, 404 (client errors) |
| Coverage | All 23 API endpoints in FmpApiClient + FMP Proxy |
SendGrid Email Delivery
| Parameter | Value |
|---|---|
| Max retries | 3 |
| Backoff schedule | 1s → 2s → 4s (exponential) |
| Retry on | 429 (rate limit), 5xx (server errors) |
| After max retries | Marked as permanently failed with error details preserved |
Twilio SMS Delivery
| Parameter | Value |
|---|---|
| Max retries | 3 |
| Backoff schedule | 1s → 2s → 4s (exponential) |
| Retry on | Any Twilio exception |
| After max retries | Marked as permanently failed with error preserved |
Stripe Payment Processing
| Parameter | Value |
|---|---|
| Payment recording retry | 2 attempts with immediate retry |
| On complete failure | FailedPaymentReconciliation record created for manual resolution |
| Double-charge prevention | Placeholder payment returned to prevent re-billing |
2. Database Connection Resilience
Azure MySQL Flexible Server connections can drop due to idle timeouts, planned failovers, or network blips. Every database context across all 11 background services and the Functions API uses EF Core's built-in retry strategy.
EnableRetryOnFailure(maxRetryCount: 3, maxRetryDelay: 5s)Services with database retry enabled:
- Azure Functions API
- Report Engine
- Report Generator
- Claude Analysis
- Email Sender
- SMS Sender
- Schedules Engine
- Download Builder
- File Purge
- Health Monitor
- Report Comparison
- Billing Engine
3. FMP API Response Caching
The FMP proxy caches API responses in memory for 5 minutes. This reduces API call costs, prevents rate limiting, and provides faster responses for frequently requested data.
| Parameter | Value |
|---|---|
| Cache duration | 5 minutes |
| Cache scope | Per-endpoint, per-query (e.g., profile?symbol=AAPL) |
| Cache storage | In-memory ConcurrentDictionary |
| Stale cleanup | Every 100 requests, entries older than 5 min are evicted |
| Observability | X-Cache: HIT or X-Cache: MISS response header |
| Benefit | 10 users viewing AAPL = 1 FMP API call (not 10) |
4. Self-Healing Background Services
All 11 background services run as systemd units with automatic restart policies. If a service crashes, it restarts within 10 seconds — no manual intervention required.
Restart=alwaysRestartSec=10The Health Monitor service checks all services every 60 seconds and alerts admins via email if any service is down.
5. Idempotent Operations
Critical operations are designed to be safe to retry without side effects:
- Billing runs:
HasSuccessfulBillingRunForPeriodAsync()prevents duplicate monthly charges - Report charges:
RetrieveJobChargeByJobIDAndAccountIDAsync()checks for existing charge before creating - Coupon usage: One-time-use enforcement per account per coupon, checked in a transaction
- Credit card default swap: Database transaction prevents race conditions
- Invoice creation:
GetInvoiceForAccountAndBillingPeriodAsync()prevents duplicate invoices
6. Graceful Degradation
When external services are unavailable, the platform degrades gracefully rather than failing completely:
- FMP API down: Reports generate with available data; missing sections show "data unavailable" instead of crashing
- Claude AI unavailable: AI analysis features show error messages; core report generation continues without AI
- Stripe down: Free accounts and coupon-covered reports still work; charges are recorded for later billing
- SendGrid down: Emails queue in database; sender retries on next cycle
- Blob storage unreachable: Service logs warning and uses placeholder mode for development
- Dashboard analytics: Each section loads independently via parallel async calls; one section failing doesn't block others
7. Health Check Endpoints
Every web application exposes a lightweight /health.html endpoint for infrastructure monitoring and CI/CD pipeline smoke tests.
| App | Endpoint | Purpose |
|---|---|---|
| Client Web | /health.html | Pipeline smoke test, load balancer probe |
| Admin Web | /health.html | Pipeline smoke test, load balancer probe |
| Technology Web | /health.html | Pipeline smoke test, load balancer probe |
| Azure Functions | /api/health | API availability check |
These endpoints return instantly without loading the full Blazor WASM framework, solving B1 tier cold-start timeout issues in CI/CD pipelines.
8. Data Protection & Recovery
- Database backups: 35-day automated retention with point-in-time restore (Azure MySQL)
- Blob storage: Zone-Redundant Storage (ZRS) — data replicated across 3 availability zones
- Deployment backups: Pipeline creates backups of console apps before each deployment
- Payment reconciliation: If Stripe charges succeed but DB recording fails,
FailedPaymentReconciliationtable preserves the record for manual resolution - GDPR data export: Full account data export available at any time via async ZIP builder
9. API Rate Limiting
Token-bucket rate limiting protects the API from abuse and ensures fair resource distribution:
| Client Type | Limit | Window |
|---|---|---|
| Authenticated users | 100 requests | Per minute, per account |
| Anonymous clients | 30 requests | Per minute, per IP |
| Webhooks (Stripe) | Unlimited | Excluded from rate limiting |
| Health checks | Unlimited | Excluded from rate limiting |
Idle rate limiters are automatically cleaned up every 10 minutes to prevent memory leaks.
Resiliency Stats
- 11 services with DB retry
- 23 FMP calls with backoff
- 5 min FMP response cache
- 35 day database backup retention
- 10 sec service auto-restart
- 3 retry attempts per operation
- 0 single points of failure