An engineering intern's journey from code that works locally to systems that survive production, covering a distributed job scheduler and multi-provider AI fallback architecture.

Most backend tutorials end at "it works." The real engineering starts when you ask what happens when it doesn't. A recent reflection from an HNG intern captures this tension perfectly: two projects, one solo and one team-based, both forced a reckoning with distributed state, infrastructure fragility, and the gap between theoretical correctness and operational survival.
The common thread across both projects is that the code was never the hard part. The hard part was understanding that distributed systems fail in ways local ones never will, and that every external dependency is a potential point of cascading failure.
The Individual Task: Building a Distributed Job Scheduler
The Problem Domain
Any backend system that handles heavy asynchronous work like email generation, batch processing, or report compilation faces the same fundamental challenge: these operations cannot block the main API thread. The standard request-response cycle has strict latency budgets, and a thirty-second email send would starve every other endpoint.
The solution is a job scheduler that manages async tasks independently from the API layer. But "independently" is doing a lot of work in that sentence. Independence means its own lifecycle, its own failure modes, its own recovery mechanisms, and its own observability story.
Architecture Choices
The scheduler was built on PostgreSQL, using a FastAPI backend with a vanilla HTML/CSS/JS frontend. The core scheduling mechanism used a MinHeap priority queue, with an alternative Timing Wheel algorithm for higher-throughput scenarios.
Several distributed systems patterns were implemented:
- DAG Dependency Resolution: Tasks that depend on other tasks (e.g., "send confirmation email only after database insert completes") form a Directed Acyclic Graph. The scheduler traverses this graph to determine execution order, which introduces complexity around partial failures and deadlock prevention.
- Dead-Letter Queue (DLQ): Tasks that fail after exhausting retries move to a DLQ for manual inspection. This prevents poison messages from blocking the queue indefinitely, a pattern common in message broker architectures like Apache Kafka and RabbitMQ.
- 3-Attempt Backoff with Jitter: Retries follow a 1s, 5s, 25s progression with random jitter. The jitter is critical: without it, failed tasks from the same batch would retry in lockstep, creating thundering herd problems that overwhelm recovering services.
- Starvation Daemon: A background process monitors tasks that have been queued but never executed, which can happen when priority inversion occurs or when the worker pool is saturated with higher-priority work.
- Server-Sent Events (SSE) Dashboard: Real-time task status streaming via SSE provides visibility without polling, which would add unnecessary load to the database.
The Deployment Nightmare

The code took hours. The deployment took a full day. This is the part that most tutorials skip, and it's where the real learning happened.
Cloud Provider Capacity Issues: Oracle Cloud was out of capacity on every free-tier shape. GCP demanded upfront payment. The intern eventually landed on an AWS t3.micro, but that's only the beginning of the story.
The SSL Chicken-and-Egg Problem: Nginx was configured to reference SSL certificates that did not yet exist. This prevented Nginx from starting. But Certbot could not run to fetch the certificates because Nginx was not running. This is a classic bootstrapping problem in infrastructure: you need the service running to get the credentials that the service needs to run. The solution was to strip the SSL config entirely, run Certbot in pure HTTP mode, and let Certbot rewrite the Nginx configuration. It's the kind of problem that teaches you more about PKI than any textbook.
Nginx Misconfiguration: Copying a full nginx.conf (including an events block) into sites-available caused Nginx to reject the configuration entirely. Nginx uses a hierarchical configuration model where events blocks belong in the main context, not in site-specific server blocks. This kind of configuration drift is a common source of production incidents.
The OOM Killer and dpkg Locks: The AWS t3.micro's 1GB of RAM meant the Linux Out-Of-Memory killer kept terminating apt during installations. Worse, an unattended background system update corrupted the docker-compose-v2 plugin and held the dpkg lock hostage. This required force-killing processes, clearing lock files, and rebuilding the package database manually. In a production environment, this is why capacity planning and memory limits matter before you even start deploying.
Docker Compose Environment Overrides: Docker Compose's environment block was completely overriding local .env variables like EMAIL_FAILURE_RATE=0.0. This is a subtle but important distinction: Compose's environment handling is not the same as Docker's, and mixing the two can produce silent configuration drift where variables you think you set are actually being overwritten.
Key Takeaway
Platform-as-a-Service tools like Railway and Heroku abstract away networking, reverse proxies, SSL certificates, DNS propagation, and Linux package management. That abstraction is valuable for speed, but it means you never learn how these systems actually work. Stripping those abstractions away is painful, but the result is understanding the full stack from the browser to the database connection string.
The Team Task: MeetMind with Multi-Provider Fallback
The Problem Domain
MeetMind is an AI-powered interview assistant that can conduct live interviews independently or assist a human interviewer in real-time. It generates summaries, candidate scorecards, and performance insights, and allows interviewers to query specific moments from the conversation.
The core challenge was architectural: MeetMind relied heavily on external LLMs to function. External AI APIs are inherently unreliable. They rate limit, time out, or throw 500 errors. A single failed API call should not crash an active interview.
The Multi-Tiered Fallback Engine
The solution was a resilient 3-step routing protocol for all external API calls:
- Primary: Attempt the request with Google Gemini
- Secondary: If Gemini fails with a retryable error after exhausting retries, fall back to OpenRouter
- Tertiary: If OpenRouter also fails, route to Groq as a final safety net
Each tier includes its own retry logic with exponential backoff. The key design decision was treating each external API as a hostile dependency: assume it will fail, plan for it, and degrade gracefully rather than crashing.
This pattern is common in distributed systems. Database connection pools use it. Load balancers use it. CDN failover uses it. The principle is the same: if you have N dependencies, you have N potential points of failure, and the probability of all of them failing simultaneously is much lower than any single one failing.
Cascading Failure Prevention
During testing, a single API outage would cascade and fail the entire interview generation process. This is the classic cascading failure pattern: a downstream dependency becomes unavailable, the upstream service retries aggressively, retries exhaust resources (connections, threads, memory), and the entire system becomes unresponsive.
The fix was not just adding fallbacks but also implementing circuit breaker patterns. If a provider fails repeatedly, the system stops sending requests to it for a cooldown period, preventing resource exhaustion and giving the provider time to recover.
Key Takeaway
Every third-party integration is a potential single point of failure. The more integrations you have, the more failure modes your system has. Engineering guardrails around external dependencies is not optional; it's a core requirement of any production system.
The Broader Pattern
Both projects illustrate the same fundamental truth about distributed systems: the happy path is an illusion.
In a local development environment, databases are always available, network latency is negligible, APIs always return 200, and memory is never constrained. In production, none of these assumptions hold.
The patterns that emerge from these experiences map directly to established distributed systems concepts:
| Pattern | Implementation | Purpose |
|---|---|---|
| Dead-Letter Queue | DLQ for failed jobs | Prevent poison messages from blocking queues |
| Circuit Breaker | Multi-tier fallback engine | Prevent cascading failures from external dependencies |
| Retry with Backoff | 1s, 5s, 25s with jitter | Handle transient failures without thundering herd |
| Priority Queue | MinHeap scheduler | Ensure critical tasks execute before lower-priority ones |
| DAG Resolution | Dependency graph traversal | Handle task ordering without deadlocks |
| Starvation Detection | Background daemon | Prevent tasks from being indefinitely deprioritized |
These are not academic patterns. They are battle-tested solutions to problems that every distributed system faces. The intern's experience of deploying on a $0 cloud server with 1GB of RAM and a broken package manager is, in many ways, a compressed version of what every production system eventually encounters.
The code was the easy part. The infrastructure, the failure modes, the operational reality of keeping a system running when things go wrong, that is where engineering actually happens.

Comments
Please log in or register to join the discussion