7 Queue Patterns That Show Up in Every FAANG Interview (And How to Actually Learn Them)

Most people learn queues for five minutes, call it “easy,” and then freeze the moment an interviewer says “now do it without a second array.”

7 Queue Patterns That Show Up in Every FAANG Interview (And How to Actually Learn Them)

I want to be honest about something before we start. For the longest time, I thought I “knew” queues. Enqueue at the back, dequeue from the front, FIFO, done, next topic. It felt like the most boring chapter in any DSA course — the one sandwiched between the exciting Stack chapter and the intimidating Tree chapter, the one you skim through because “how hard can a line at a ticket counter really be?”

Then I sat in an interview where the question was: “Implement a queue using two stacks, and tell me the time complexity of each operation.”

And I just… sat there. I knew what a stack was. I knew what a queue was. I had absolutely no idea how to make one out of the other. That thirty seconds of silence taught me more about queues than any tutorial ever had.

So this is the guide I wish someone had handed me before that interview. Not a copy-paste of definitions, but the actual patterns that keep showing up — in interviews, in real systems, in your own code without you even noticing.

Quick honest question before we dive in: when’s the last time you actually thought about how a queue works under the hood, instead of just calling .push() and .shift() and moving on? Be honest in the comments, I promise I won't judge — I didn't either, for years.


First, What Even Is a Queue?

Think of the billing counter at a supermarket. First person in line gets billed first. Nobody gets to cut in front (unless you’re that one person, and yes, we all silently judge that person). That’s it. That’s a queue. First In, First Out — FIFO.

In JavaScript, the laziest way to build one looks like this:

class NaiveQueue {
  constructor() {
    this.items = [];
  }
enqueue(value) {
    this.items.push(value); // add to the back
  }
  dequeue() {
    return this.items.shift(); // remove from the front
  }
  peek() {
    return this.items[0];
  }
  isEmpty() {
    return this.items.length === 0;
  }
}

Looks fine, right? It even works. But here’s the part almost nobody tells beginners:

Array.prototype.shift() is O(n), not O(1).

Why? Because when you remove the first element of a JS array, every single remaining element has to shift one position to the left in memory. If you’re processing a queue with 100,000 items, you’re doing roughly 100,000 + 99,999 + 99,998… operations instead of 100,000. That’s the difference between your code running instantly and your code timing out on a big input.

Did you know that, or did you just find out right now like I once did while debugging a “mysteriously slow” job processor at 1 AM? That single line was the entire bug.


Pattern 1: The Real O(1) Queue (Using an Object as a Map)

The fix is almost embarrassingly simple once you see it. Instead of shifting the array, just track a front pointer and treat deletions as “move the pointer,” not “physically remove the element.”

class Queue {
  constructor() {
    this.items = {};
    this.front = 0;
    this.back = 0;
  }
enqueue(value) {
    this.items[this.back] = value;
    this.back++;
  }
  dequeue() {
    if (this.isEmpty()) return undefined;
    const value = this.items[this.front];
    delete this.items[this.front];
    this.front++;
    return value;
  }
  peek() {
    return this.items[this.front];
  }
  isEmpty() {
    return this.front === this.back;
  }
}

Now every operation is truly O(1). No shifting, no wasted work. This is the version I’d actually use in production code, and it’s the version most people never learn because tutorials stop at the “easy” array version.


Pattern 2: Circular Queue — When Memory Isn’t Free

Here’s a scenario: you’re building a fixed-size buffer for streaming data — say, the last 10 sensor readings from an IoT device, or a rolling log of the last 50 actions a user took. You don’t want the buffer growing forever. You want it to wrap around and overwrite old data once it’s full.

That’s a circular queue. Instead of the “back” pointer just growing infinitely, it wraps back to the start using modulo.

class CircularQueue {
  constructor(capacity) {
    this.capacity = capacity;
    this.items = new Array(capacity);
    this.front = 0;
    this.size = 0;
  }
enqueue(value) {
    if (this.size === this.capacity) {
      throw new Error("Queue is full");
    }
    const insertIndex = (this.front + this.size) % this.capacity;
    this.items[insertIndex] = value;
    this.size++;
  }
  dequeue() {
    if (this.size === 0) return undefined;
    const value = this.items[this.front];
    this.front = (this.front + 1) % this.capacity;
    this.size--;
    return value;
  }
}

That % this.capacity line is doing all the magic — it's what lets index 9 wrap right back around to index 0 in a 10-slot buffer. This exact pattern quietly runs inside circular buffers used in networking, audio processing, and CPU task scheduling. Ever wondered how your music app never runs out of buffer space even though it's streaming infinite audio? This is roughly why.


Pattern 3: Queue Using Two Stacks (The Interview Classic)

This is the question that got me. And once you see the trick, you’ll never forget it — I promise.

The idea: use one stack (inStack) purely for enqueue, and one stack (outStack) purely for dequeue. When outStack is empty, dump everything from inStack into it — this reverses the order, turning "last in" into "first out," which is exactly what a queue needs.

class QueueUsingStacks {
  constructor() {
    this.inStack = [];
    this.outStack = [];
  }
  enqueue(value) {
    this.inStack.push(value);
  }
  dequeue() {
    if (this.outStack.length === 0) {
      while (this.inStack.length > 0) {
        this.outStack.push(this.inStack.pop());
      }
    }
    return this.outStack.pop();
  }
}

The beautiful part: each element only ever gets moved from inStack to outStack once in its lifetime. So even though a single dequeue() call could look like O(n) in the worst case, the amortized cost across many operations is still O(1). That word — amortized — is exactly what interviewers want to hear you say. Say it. Trust me.


Pattern 4: Deque — When You Need Both Ends

A deque (say it “deck”) lets you add and remove from both the front and the back. JavaScript arrays already give you this for free with push, pop, unshift, and shift — the catch is unshift and shift are O(n) for the same reason as before, so for performance-heavy code, people build deques on doubly linked lists instead.

Where does this actually matter? Sliding window problems. Which brings us to the pattern that, in my experience, separates people who “know queues” from people who can actually use them under pressure.


Pattern 5: The Monotonic Queue (Sliding Window Maximum)

Question for you: given an array and a window size k, how do you find the maximum of every window, without recalculating the max from scratch every single time?

The brute-force answer is O(n·k). It works. It also gets you rejected if n is large. The elegant answer uses a monotonic deque — a deque that always stays in decreasing order from front to back.

function maxSlidingWindow(nums, k) {
  const result = [];
  const deque = []; // stores indices, values stay decreasing
  for (let i = 0; i < nums.length; i++) {
    // remove indices that are out of this window
    if (deque.length && deque[0] <= i - k) {
      deque.shift();
    }
    // remove smaller values from the back - they can never be the max now
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
    deque.push(i);
    if (i >= k - 1) {
      result.push(nums[deque[0]]); // front is always the current max
    }
  }
  return result;
}

The genius of this: every index enters and leaves the deque at most once, so the whole thing runs in O(n), not O(n·k). 

The first time I understood why this works — that we’re throwing away values that can never possibly be the max again — genuinely felt like a lightbulb moment. Has any DSA pattern ever done that for you, made something click so hard you actually said “oh!” out loud? This one did it for me.


Pattern 6: Priority Queue (When Order ≠ Arrival Time)

Sometimes FIFO isn’t what you want at all. Sometimes you want “process the most urgent thing first,” regardless of when it arrived. That’s a priority queue, usually built on a heap.

JavaScript, annoyingly, has no built-in heap or priority queue (unlike Python’s heapq or Java's PriorityQueue). So people either hand-roll a min-heap or reach for a library like heap-js or tinyqueue. Here's a minimal min-heap to show the idea:

class MinHeap {
  constructor() {
    this.heap = [];
  }
  enqueue(value) {
    this.heap.push(value);
    this.#bubbleUp(this.heap.length - 1);
  }
  dequeue() {
    const top = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.#bubbleDown(0);
    }
    return top;
  }
  #bubbleUp(index) {
    while (index > 0) {
      const parent = Math.floor((index - 1) / 2);
      if (this.heap[parent] <= this.heap[index]) break;
      [this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
      index = parent;
    }
  }
  #bubbleDown(index) {
    const n = this.heap.length;
    while (true) {
      let smallest = index;
      const left = 2 * index + 1;
      const right = 2 * index + 2;
      if (left < n && this.heap[left] < this.heap[smallest]) smallest = left;
      if (right < n && this.heap[right] < this.heap[smallest]) smallest = right;
      if (smallest === index) break;
      [this.heap[smallest], this.heap[index]] = [this.heap[index], this.heap[smallest]];
      index = smallest;
    }
  }
}

This exact structure is what powers Dijkstra’s shortest path algorithm and “find the K closest points” type questions. Every time your GPS calculates the fastest route, something conceptually close to this is running under the hood.


Pattern 7: BFS — The Pattern That Shows Up Everywhere

If you only remember one thing from this whole article, make it this: whenever you see the words “shortest path,” “minimum steps,” or “level by level,” your brain should immediately think Queue.

Breadth-First Search uses a queue to explore a graph or tree one layer at a time. Here’s the skeleton — the same skeleton behind “rotting oranges,” “word ladder,” and “binary tree level order traversal”:

function bfs(startNode, graph) {
  const visited = new Set([startNode]);
  const queue = [startNode];
  const order = [];
  while (queue.length > 0) {
    const current = queue.shift();
    order.push(current);
    for (const neighbor of graph[current]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
  return order;
}

The queue guarantees you finish exploring everything at distance 1 before you even touch anything at distance 2. That’s the entire reason BFS finds the shortest path in an unweighted graph. Once you see the queue as “the thing enforcing level-by-level order,” half of graph and tree problems stop feeling scary.


A Quick Real-World Detour

Zoom out from LeetCode for a second. 

The same core idea — “handle things in the order they arrived, decouple the sender from the receiver” — is exactly what tools like Kafka, RabbitMQ, and AWS SQS do at massive scale. 

When you place an Amazon order, your request doesn’t get processed instantly by one giant function. It gets dropped into a queue, and a separate worker picks it up when it’s ready. Same pattern. Just bigger, and with way more infrastructure around it. Funny how the “boring beginner topic” turns out to be running half the internet, isn’t it?


Questions to Practice, Organized by Pattern

  • Basic Queue: Implement Queue using Arrays / Design Circular Queue (LeetCode 622)

  • Two Stacks: Implement Queue using Stacks (LeetCode 232), Implement Stack using Queues (LeetCode 225)

  • Deque: Design Circular Deque (LeetCode 641)

  • Monotonic Queue: Sliding Window Maximum (LeetCode 239), Shortest Subarray with Sum at Least K (LeetCode 862)

  • Priority Queue: Kth Largest Element in a Stream (LeetCode 703), Top K Frequent Elements (LeetCode 347), Merge K Sorted Lists (LeetCode 23)

  • BFS: Binary Tree Level Order Traversal (LeetCode 102), Rotting Oranges (LeetCode 994), Word Ladder (LeetCode 127), Number of Islands (LeetCode 200)

Do these roughly in this order. Each one builds intuition for the next.


Where I Landed

Here’s the mental model I actually use now, months after that interview that humbled me: a queue is for “fair, in-order processing.” A stack is for “undo the most recent thing.” A priority queue is for “urgency beats arrival time.” And almost every “hard” graph or tree problem that mentions “shortest” or “minimum” is secretly just BFS wearing a costume.

None of these patterns are individually hard. What’s hard is that nobody tells you they’re patterns until you’ve already failed an interview because of one. So consider this your heads-up — the one I didn’t get.

What tripped you up the most here — the two-stack trick, the monotonic deque, or something I didn’t even cover? Genuinely curious, drop it in the comments — I read every single one.