Balaji Srinivasan

In the Age of AI, I Built a Retry Queue by Hand — and It Felt Amazing

6 minutes (1442 words)

Some weekends, you just want to build something uselessly beautiful.

For me, that meant writing a lightweight retry proxy — a small layer that would sit between my service and a flaky third-party API.

Nothing enterprise-grade. No Kafka, no frameworks, no resilience libraries.

Just Java, a terminal, and curiosity.

It wasn't meant to be useful.

It was meant to remind me what useful code feels like.

🔗When All You Want Is "Try Again Later"

The idea was simple:

  1. Forward API requests downstream.
  2. If they fail, queue them for retry.
  3. Process retries in the background until success or timeout.

A tiny open-source-style playground — the kind you'd hack together between cups of coffee.

🔗The Old Reflex: "I'll Build My Own Linked List"

My muscle memory kicked in:

class RetryNode {
    Request request;
    RetryNode next;
}

I could've chained these up, managed head/tail pointers, and built my own in-memory queue.

But halfway through sketching it out, I stopped.

I wasn't trying to prove I could build data structures.

I wanted to prove I could still use the language well.

Java already gives us decades of concurrency-safe, memory-efficient primitives.

I didn't need to out-engineer them — I just needed to rediscover them.

So I reached for an old friend I'd long ignored:

Deque<Request> retryQueue = new ArrayDeque<>();

🔗Designing the Retry Queue (and Why It Works)

Once the core idea took shape, I stepped back to sketch how requests would actually move through the system.

That's when this simple sequence diagram brought everything together:

Retry Queue Sequence Diagram

At a glance, it shows exactly what matters:

Clear boundaries — each participant has one role:

Intent made visible — every arrow is readable:

Graceful failure flow — the alt branches tell the real story of resilience:

No frameworks, no annotations — just clean, visible behavior.

🔗The Java Shape Behind the Diagram

Here's how that design translated directly into code:

class RetryProxy {
    private final Deque<Request> retryQueue = new ArrayDeque<>();
    private final ScheduledExecutorService scheduler =
        Executors.newScheduledThreadPool(2);

    void send(Request req) {
        try {
            forward(req);
        } catch (IOException e) {
            System.out.println("Failed: " + req.id + " → queued for retry");
            retryQueue.addLast(req); // enqueue on failure
        }
    }

    void start() {
        scheduler.scheduleAtFixedRate(this::processQueue, 0, 2, TimeUnit.SECONDS);
    }

    private void processQueue() {
        Request req = retryQueue.pollFirst();
        if (req == null) return;

        try {
            forward(req);
        } catch (IOException e) {
            req.incrementRetries();
            if (req.shouldRetry()) retryQueue.addLast(req); // re-enqueue
        }
    }

    private void forward(Request req) throws IOException {
        // Simulate network call to ThirdPartyAPI
    }
}

🔗Seeing Behavior, Not Just Code

Watching that retry loop come alive felt like watching a small ecosystem breathe:

It's not complex, but it's alive — and you can reason about every transition. That's the difference between code that "runs" and code that tells a story.

🔗The Temptation to Overbuild

There's something seductive about low-level control. Every seasoned engineer has that voice in their head whispering:

You can make this faster… tighter… smarter.

But I've learned that every time I rewrite something the JDK already does, I'm not expressing mastery — I'm expressing distrust.

Java's ArrayDeque already solves this problem elegantly:

It's been optimized by people who think about micro-architectural memory access patterns for fun.

All I had to do was use it with intention.

This project wasn't about proving depth — it was about practicing restraint.

Re-implementing a queue in 2025 doesn't make you clever; using Deque well does.

Java's standard library already encapsulates the right trade-offs:

When the language offers something beautifully simple, sometimes the smartest thing you can do is just say thank you and use it.

🔗Inside Java's Deque: What Makes It Elegant

The more I used Deque, the more I appreciated how well it captures what good APIs should be — simple at the surface, powerful in composition.

At its core, Deque (Double-Ended Queue) is exactly what it sounds like:

a linear collection supporting insertion and removal at both ends.

That one design choice unlocks a world of patterns.

What I love is how expressive the method names are.

They don't hide complexity behind abstractions — they describe behavior.

You can almost read your concurrency logic aloud and know what's happening.

retryQueue.addLast(req);        // enqueue at end
retryQueue.removeFirst();       // oldest first
retryQueue.addFirst(urgentReq); // bump priority

🔗Deque Implementations: Choosing the Right One

Not all Deques are created equal — and understanding their trade-offs is what separates a clean solution from a latent performance bug.

Deque Implementation Decision Tree

The decision tree above captures the key questions:

Is it single-threaded?

No — Do you need blocking semantics?

🔗Practical Use Cases of Deque

Once you start noticing it, Deque is everywhere. It's one of those invisible workhorses that quietly keeps modern Java frameworks humming — from messaging systems to caches to Spring internals.

It's not the hero of the architecture, but it's part of the infrastructure of elegance.

🔗Retry Buffers and Back Pressure Loops

Your classic use case — queue up failed requests, re-enqueue on failure, process oldest first — is the same principle that powers many internal request pipelines.

🔗Apache Kafka

Kafka's network client layer maintains a Deque per connection to track outstanding requests:

🔗Annotation Traversal in Spring Framework

Spring itself leans on Deque in multiple modules — not publicly exposed, but deeply embedded in core utilities.

Annotation scanning:

🔗Why It Matters — Even When AI Can Write the Code

We've reached a point where AI coding tools don't just autocomplete lines — they generate entire working implementations.

Ask Claude, Cursor, or Copilot to "build a retry mechanism," and you'll get a runnable solution in seconds.

But the real question isn't can it build it — it's will it build it right?

Because here's the truth: if you don't know what a Deque is, and if you don't understand why it's better for ordered, reversible, or bounded workflows — then you can't tell the AI to use it.

You'll get a perfectly valid queue, or maybe a synchronized List, and it'll even pass tests. But it won't reflect intent. It won't express the system's rhythm — that subtle, almost architectural decision between order and flow.

AI can write a retry queue.

Only you can shape it into a resilient one.

That's why fundamentals still matter.

Not because we need to hand-craft everything — but because we need the language to direct these tools.

When I asked an AI assistant to generate a retry layer, it produced a BlockingQueue loop — correct, functional, but not expressive.

When I said "use a Deque for retry ordering and prioritization", the design immediately improved.

That difference — between asking for code and asking for the right code — comes only from understanding.

Tags: #engineering #craftsmanship #java #fundamentals