Definition
- Queue – a linear data structure that follows First In First Out (FIFO) ordering, meaning the element that has been waiting the longest is always served first.
- enqueue / offer – adds an element at the rear of the queue in O(1) time.
- dequeue / poll – removes and returns the element at the front in O(1) time.
- peek – returns the front element without removing it in O(1) time.
- ArrayDeque – the recommended general-purpose Queue implementation in Java, backed by a resizable circular array.
- BlockingQueue – a thread-safe queue that blocks when empty on poll or when full on offer, used for producer-consumer patterns.
Imagine a line at the National Bank of India cash counter. Rajesh walks in first and stands at the counter. Then Priya arrives and stands behind him. When the teller is ready, Rajesh is served first because he arrived first, and Priya moves up. Nobody can jump the line. Whoever joined earliest at the back gets served earliest at the front. That orderly line is exactly a queue.
Understanding the Concepts
What is a queue?
A queue is a linear data structure that follows First In First Out ordering. Elements are added at one end called the rear and removed from the other end called the front. The element that has been waiting the longest is always the next one served. This makes a queue the natural choice whenever fairness and ordering matter.
package com.bank.core;
import java.util.ArrayDeque;
import java.util.Queue;
public class TransferQueue {
public static void main(String[] args) {
Queue<String> pending = new ArrayDeque<>();
pending.offer("TXN-1001: Rajesh transfer 50000");
pending.offer("TXN-1002: Priya transfer 25000");
pending.offer("TXN-1003: Pankaj transfer 75000");
System.out.println("Front: " + pending.peek());
System.out.println("Served: " + pending.poll());
System.out.println("Next: " + pending.peek());
}
}
Front: TXN-1001: Rajesh transfer 50000
Served: TXN-1001: Rajesh transfer 50000
Next: TXN-1002: Priya transfer 25000
Why do we need a queue?
A queue exists because many real systems need to process items in the order they arrive. A bank cannot serve the customer who walked in last while someone who arrived an hour ago is still waiting. A message broker cannot deliver message number 5 before message number 1. FIFO ordering guarantees fairness, and a queue enforces it structurally.
package com.bank.service;
import java.util.ArrayDeque;
import java.util.Queue;
public class LoanApplicationQueue {
public static void main(String[] args) {
Queue<String> applications = new ArrayDeque<>();
applications.offer("LOAN-2001: Rajesh home loan");
applications.offer("LOAN-2002: Priya car loan");
applications.offer("LOAN-2003: Pankaj education loan");
System.out.println("Processing in FIFO order:");
while (!applications.isEmpty()) {
System.out.println(" Processing: " + applications.poll());
}
}
}
Processing in FIFO order:
Processing: LOAN-2001: Rajesh home loan
Processing: LOAN-2002: Priya car loan
Processing: LOAN-2003: Pankaj education loan
How does a queue work internally?
ArrayDeque uses a resizable circular array. Two pointers, head and tail, track the front and rear of the queue. When you offer, the element is placed at the tail position and the tail advances. When you poll, the element at the head is returned and the head advances. Both pointers wrap around when they reach the end of the array, which is why it is called circular. When the array fills, it doubles in size, giving amortized O(1) cost.
package com.bank.core;
// Simplified circular queue to illustrate internals
public class CircularQueue {
private String[] data;
private int head = 0;
private int tail = 0;
private int size = 0;
public CircularQueue(int capacity) {
data = new String[capacity];
}
public void offer(String item) {
if (size == data.length) throw new RuntimeException("Queue full");
data[tail] = item;
tail = (tail + 1) % data.length;
size++;
}
public String poll() {
if (size == 0) throw new RuntimeException("Queue empty");
String item = data[head];
data[head] = null;
head = (head + 1) % data.length;
size--;
return item;
}
public static void main(String[] args) {
CircularQueue q = new CircularQueue(4);
q.offer("SAV-1001");
q.offer("SAV-1002");
System.out.println("Polled: " + q.poll());
q.offer("SAV-1003");
System.out.println("Polled: " + q.poll());
System.out.println("Polled: " + q.poll());
}
}
Polled: SAV-1001
Polled: SAV-1002
Polled: SAV-1003
How do you create and use a queue in Java?
Java provides the Queue interface in java.util. The two main implementations are ArrayDeque and LinkedList. Prefer ArrayDeque for most use cases because it has better cache locality and no node-allocation overhead. Use offer and poll instead of add and remove because they return null or false on failure rather than throwing exceptions.
package com.bank.service;
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;
public class QueueApiDemo {
public static void main(String[] args) {
// Preferred: ArrayDeque
Queue<String> queue1 = new ArrayDeque<>();
queue1.offer("FT-101");
queue1.offer("FT-102");
System.out.println("ArrayDeque poll: " + queue1.poll());
// Alternative: LinkedList
Queue<String> queue2 = new LinkedList<>();
queue2.offer("FT-201");
queue2.offer("FT-202");
System.out.println("LinkedList poll: " + queue2.poll());
// Safe: poll returns null on empty queue
Queue<String> empty = new ArrayDeque<>();
System.out.println("Poll empty: " + empty.poll()); // null, no exception
}
}
ArrayDeque poll: FT-101
LinkedList poll: FT-201
Poll empty: null
How is a queue different from a stack?
A queue is FIFO and a stack is LIFO. A queue serves the oldest element first, while a stack serves the newest. Both are linear structures with O(1) core operations, but they model opposite real-world patterns.
| Feature | Queue (FIFO) | Stack (LIFO) |
|---|---|---|
| Order | First In First Out | Last In First Out |
| Add | offer / enqueue (rear) | push (top) |
| Remove | poll / dequeue (front) | pop (top) |
| Use case | BFS, task scheduling, fairness | DFS, undo, expression eval |
| Java class | ArrayDeque (offer/poll) | ArrayDeque (push/pop) |
package com.bank.core;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
public class QueueVsStack {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
queue.offer("First");
queue.offer("Second");
System.out.println("Queue serves: " + queue.poll()); // First
Deque<String> stack = new ArrayDeque<>();
stack.push("First");
stack.push("Second");
System.out.println("Stack serves: " + stack.pop()); // Second
}
}
Queue serves: First
Stack serves: Second
What are the different types of queues?
Java offers several queue variants. A PriorityQueue serves elements by priority rather than arrival order. A Deque (double-ended queue) allows add and remove at both ends. A BlockingQueue blocks the calling thread when the queue is empty or full, which is ideal for producer-consumer patterns in banking transaction processing.
package com.bank.service;
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityLoanQueue {
public static void main(String[] args) {
// PriorityQueue: smallest first by default
Queue<Integer> urgency = new PriorityQueue<>();
urgency.offer(3); // low priority
urgency.offer(1); // high priority
urgency.offer(2); // medium priority
System.out.println("Serve by priority:");
while (!urgency.isEmpty()) {
System.out.println(" Priority " + urgency.poll());
}
}
}
Serve by priority:
Priority 1
Priority 2
Priority 3
What exceptions can occur?
Calling remove() or element() on an empty queue throws NoSuchElementException. That is why poll() and peek() are preferred: they return null instead. For a BlockingQueue, put() and take() block rather than throwing, which is safer for concurrent code.
package com.bank.core;
import java.util.ArrayDeque;
import java.util.NoSuchElementException;
import java.util.Queue;
public class QueueExceptionDemo {
public static void main(String[] args) {
Queue<String> queue = new ArrayDeque<>();
// Wrong: remove() throws on empty
try {
queue.remove();
} catch (NoSuchElementException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
// Correct: poll() returns null on empty
String result = queue.poll();
System.out.println("Poll on empty: " + result);
}
}
Caught: NoSuchElementException
Poll on empty: null
What about thread safety?
ArrayDeque and LinkedList are not thread-safe. For concurrent producer-consumer patterns, use java.util.concurrent.LinkedBlockingQueue or ConcurrentLinkedQueue. In a banking system where one thread receives transfer requests and another processes them, a BlockingQueue is the natural bridge.
package com.bank.service;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class TransferProcessor {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> transfers = new LinkedBlockingQueue<>(100);
// Producer thread
transfers.put("NEFT-3001: Rajesh to Priya 25000");
transfers.put("NEFT-3002: Pankaj to Rajesh 50000");
// Consumer thread
System.out.println("Processing: " + transfers.take());
System.out.println("Processing: " + transfers.take());
}
}
Processing: NEFT-3001: Rajesh to Priya 25000
Processing: NEFT-3002: Pankaj to Rajesh 50000
What should a senior engineer know?
Queues are the backbone of asynchronous systems. Distributed message brokers like Kafka, RabbitMQ, and Amazon SQS are durable queues that decouple producers from consumers. When the National Bank of India processes millions of salary-day transactions, it enqueues each request and worker threads dequeue them at a safe rate, smoothing out traffic spikes while preserving FIFO ordering.
The key architectural insight is that a queue decouples the speed of producers from the speed of consumers. The front end can accept requests as fast as they arrive and drop them into the queue, while the backend processes them steadily. This is the same pattern whether the queue is an in-memory ArrayDeque or a distributed Kafka topic with retention and replay.
package com.bank.service;
import java.util.ArrayDeque;
import java.util.Queue;
public class SalaryDayProcessor {
public static void main(String[] args) {
Queue<String> salaryTransfers = new ArrayDeque<>();
// Simulate burst of salary-day requests
for (int i = 1; i <= 5; i++) {
String txn = "SAL-" + i + ": Credit salary to EMP-" + (1000 + i);
salaryTransfers.offer(txn);
System.out.println("Enqueued: " + txn);
}
System.out.println("\nProcessing at steady rate:");
while (!salaryTransfers.isEmpty()) {
System.out.println(" " + salaryTransfers.poll());
}
}
}
Enqueued: SAL-1: Credit salary to EMP-1001
Enqueued: SAL-2: Credit salary to EMP-1002
Enqueued: SAL-3: Credit salary to EMP-1003
Enqueued: SAL-4: Credit salary to EMP-1004
Enqueued: SAL-5: Credit salary to EMP-1005
Processing at steady rate:
SAL-1: Credit salary to EMP-1001
SAL-2: Credit salary to EMP-1002
SAL-3: Credit salary to EMP-1003
SAL-4: Credit salary to EMP-1004
SAL-5: Credit salary to EMP-1005
Visual Overview
Queue Operations (FIFO) ───────────────────────── offer(A) offer(B) offer(C) ┌─────┐ ┌─────┬─────┐ ┌─────┬─────┬─────┐ │ A │ │ A │ B │ │ A │ B │ C │ └─────┘ └─────┴─────┘ └─────┴─────┴─────┘ front front front rear poll() → returns A (first in, first out) ┌─────┬─────┐ │ B │ C │ └─────┴─────┘ front rear
Circular Array (ArrayDeque internals)
─────────────────────────────────────
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ │ │ A │ B │ C │ │ │ │
└────┴────┴────┴────┴────┴────┴────┴────┘
▲ ▲
│ │
head tail
(front) (rear)
offer → element at tail, tail advances right
poll → element at head returned, head advances right
Wraps around when reaching the end of the array
Key Takeaway
A queue is a linear data structure that follows the First In First Out principle. You add elements at the rear and remove them from the front, which guarantees that the element waiting the longest is always served first. This makes a queue the natural choice whenever fairness and ordering matter, from processing customer requests to handling asynchronous messages.
In my banking project at the National Bank of India, I used a queue to process fund transfer requests. On salary day, thousands of NEFT requests arrive within minutes, and processing them all at once would overwhelm the core banking system. Instead, each incoming request is offered to a queue, and a pool of worker threads polls and processes them one at a time at a safe rate. Because of the FIFO guarantee, if Rajesh submits his transfer one second before Priya, his request is always processed first, and no customer feels cheated.
In Java, I implement queues using ArrayDeque through the Queue interface, and I prefer the offer and poll methods over add and remove because they return null or false on failure rather than throwing exceptions. For concurrent producer-consumer patterns, I use LinkedBlockingQueue from java.util.concurrent, which blocks the consuming thread when the queue is empty and the producing thread when the queue is full, giving me backpressure for free. At scale, the same idea extends to distributed message brokers like Kafka and RabbitMQ, which are durable queues that decouple entire services.
The key insight a senior engineer should communicate is that a queue decouples the speed of producers from the speed of consumers. The front end can accept requests as fast as they arrive and drop them into the queue, while the backend processes them at a steady, safe rate. That decoupling is the backbone of almost every asynchronous banking and trading system.
Remember one sentence: a queue is First In First Out, where offer adds at the rear, poll removes from the front, and the longest-waiting element is always served next.
---