← Open in the full interactive course (progress tracking, search & more)

A Stack gives you back what you put in last. A Queue gives you back what you put in first. The order you retrieve things is the entire point of both.

Picture two everyday situations. First: a stack of plates in a cupboard — you always take the top plate, the one most recently placed there, and new clean plates go on top too. Second: a line of people waiting at a coffee shop counter — the first person to join the line is the first one served, no matter how long the line gets. Both are "add things, then take them out later" collections. What differs completely is which item comes out.

In this lesson, you'll learn about Stack<T> and Queue<T> — two collections defined entirely by their retrieval order, and where each shows up constantly in real software.

What Is It?

The Simple Explanation

A Stack<T> is Last In, First Out (LIFO) — the most recently added item is always the first one you get back, like that pile of plates. A Queue<T> is First In, First Out (FIFO) — the earliest added item is always the first one you get back, like that coffee shop line.

The Technical Definition

Both Stack<T> and Queue<T> are generic collections in System.Collections.Generic that deliberately restrict how you access their elements — unlike List<T>, neither lets you index into an arbitrary position. Instead, each exposes only the operations needed to add one item and retrieve/remove the "next" item, where "next" is defined by their opposite ordering rules.

OperationStack<T> (LIFO)Queue<T> (FIFO)
Add an itemPush(item)Enqueue(item)
Remove and return the "next" itemPop() — most recently pushedDequeue() — earliest enqueued
Look without removingPeek()Peek()
Check if empty first (safe)TryPop(out T)TryDequeue(out T)

Why Does It Exist?

The Problem

Suppose you're building an "undo" feature for a text editor. Every action the user takes needs to be reversible, and — critically — undoing must always reverse the most recent action first. You could try to manage this with a List<T>:

List<string> actions = []; actions.Add("Typed 'Hello'"); actions.Add("Typed ' World'"); // To undo, you'd have to remember to always grab the LAST element yourself: string lastAction = actions[actions.Count - 1]; // easy to get this index wrong actions.RemoveAt(actions.Count - 1);

This works, but nothing about a List<T> stops you from accidentally reading actions[0] instead, undoing the first action ever taken instead of the most recent one. The "must always be the last one" rule lives only in the programmer's head, not in the type itself.

The Solution

Stack<T> bakes that rule directly into the type — there's no index to get wrong, because there's no indexer at all:

Stack<string> actions = []; actions.Push("Typed 'Hello'"); actions.Push("Typed ' World'"); string lastAction = actions.Pop(); // always the most recent — guaranteed by the type itself

Similarly, a background job processor needs to handle print jobs in the exact order they were submitted — the first job in line should print first, no exceptions. Queue<T> makes that guarantee structurally, the same way Stack<T> guarantees "most recent first."

Big Picture

STACK (LIFO) vs QUEUE (FIFO)
Stack<T> — LIFO
Push A → [A]
Push B → [A, B]
Push C → [A, B, C]
Pop() → returns C (last pushed)
Last In, First Out
Queue<T> — FIFO
Enqueue A → [A]
Enqueue B → [A, B]
Enqueue C → [A, B, C]
Dequeue() → returns A (first enqueued)
First In, First Out
Same three items (A, B, C) pushed/enqueued in the same order — completely different retrieval order.

How It Works

STACK<T>, STEP BY STEP
1. PUSH ITEMS ON
Stack<string> history = [];
history.Push("Page A");
history.Push("Page B");
history.Push("Page C");
2. PEEK WITHOUT REMOVING, THEN POP
string top = history.Peek();   // "Page C" — still there, just looked at
string popped = history.Pop(); // "Page C" — removed and returned
3. GUARD AGAINST AN EMPTY STACK
if (history.TryPop(out string? previous))
    Console.WriteLine($"Going back to {previous}");
else
    Console.WriteLine("No history to go back to.");
QUEUE<T>, STEP BY STEP
1. ENQUEUE ITEMS
Queue<string> printJobs = [];
printJobs.Enqueue("Report.pdf");
printJobs.Enqueue("Invoice.pdf");
printJobs.Enqueue("Photo.png");
2. PEEK AND DEQUEUE
string next = printJobs.Peek();     // "Report.pdf" — the earliest one, still there
string printed = printJobs.Dequeue(); // "Report.pdf" — removed and returned
3. PROCESS EVERYTHING IN ORDER
while (printJobs.Count > 0)
{
    string job = printJobs.Dequeue();
    Console.WriteLine($"Printing {job}...");
}

Simple Example

// Stack: an editor's undo history Stack<string> undoHistory = []; undoHistory.Push("Typed 'Hello'"); undoHistory.Push("Typed ' World'"); undoHistory.Push("Deleted 'World'"); Console.WriteLine(undoHistory.Pop()); // "Deleted 'World'" — most recent action, undone first Console.WriteLine(undoHistory.Pop()); // "Typed ' World'" // Queue: a coffee shop's order line Queue<string> orderLine = []; orderLine.Enqueue("Amy's order"); orderLine.Enqueue("Ben's order"); orderLine.Enqueue("Cara's order"); Console.WriteLine(orderLine.Dequeue()); // "Amy's order" — first in line, served first Console.WriteLine(orderLine.Dequeue()); // "Ben's order"

Code → Meaning → Result:

Real-World Example

A background service processes print jobs submitted by many office computers, always in the order they arrived — a perfect fit for Queue<T>. Meanwhile, a document editor built alongside it needs an undo stack — a perfect fit for Stack<T>.

public record PrintJob(string FileName, string RequestedBy); public class PrintSpooler { private readonly Queue<PrintJob> _jobs = []; public void Submit(PrintJob job) { _jobs.Enqueue(job); Console.WriteLine($"Queued: {job.FileName} (from {job.RequestedBy})"); } public void ProcessNext() { if (_jobs.TryDequeue(out PrintJob? job)) Console.WriteLine($"Printing: {job.FileName} for {job.RequestedBy}"); else Console.WriteLine("No jobs waiting."); } public int PendingJobs => _jobs.Count; } var spooler = new PrintSpooler(); spooler.Submit(new PrintJob("Q3-Report.pdf", "Amy")); spooler.Submit(new PrintJob("Invoice-4821.pdf", "Ben")); spooler.ProcessNext(); // prints Amy's job — it was submitted first spooler.ProcessNext(); // prints Ben's job next // Queued: Q3-Report.pdf (from Amy) // Queued: Invoice-4821.pdf (from Ben) // Printing: Q3-Report.pdf for Amy // Printing: Invoice-4821.pdf for Ben
public class UndoManager { private readonly Stack<string> _actions = []; public void RecordAction(string description) { _actions.Push(description); Console.WriteLine($"Did: {description}"); } public void Undo() { if (_actions.TryPop(out string? lastAction)) Console.WriteLine($"Undoing: {lastAction}"); else Console.WriteLine("Nothing to undo."); } } var editor = new UndoManager(); editor.RecordAction("Typed 'Hello World'"); editor.RecordAction("Applied bold formatting"); editor.Undo(); // undoes the formatting — the MOST RECENT action editor.Undo(); // undoes the typing // Did: Typed 'Hello World' // Did: Applied bold formatting // Undoing: Applied bold formatting // Undoing: Typed 'Hello World'

Analogy

A Plate Stack and a Checkout Line

A cafeteria's plate dispenser is a physical stack: the spring pushes the most recently added plate to the top, and that's the one every customer grabs. Nobody can reach in and pull a plate from the bottom without disturbing everything above it — the mechanism itself enforces "last in, first out."

A checkout line at a grocery store is a physical queue: whoever joined the line first gets served first, no matter how long the line grows behind them. Cutting to the front is the social equivalent of trying to Dequeue from the wrong end — the structure of a real line simply doesn't allow it.

Under the Hood

HOW STACK<T> AND QUEUE<T> ARE IMPLEMENTED
1. BOTH ARE BUILT ON AN ARRAY, LIKE LIST<T>
2. STACK: PUSH/POP FROM ONE END ONLY
3. QUEUE: ENQUEUE AND DEQUEUE FROM OPPOSITE ENDS
4. NEITHER SUPPORTS RANDOM ACCESS BY DESIGN

Common Confusion

1. Which method name goes with which type

It's genuinely easy to mix up Push/Pop (Stack) with Enqueue/Dequeue (Queue), especially early on. A memory trick: a stack is something you physically push down on and pop off of (plates, a stack of papers). A queue is a British word for a waiting line, so people queue up (enqueue) and eventually get dequeued from the front.

2. "It's just a List with different method names" — not quite

The restricted API isn't just a style choice — it's the whole point. A List<T> lets you insert or remove anywhere, which means nothing in the type itself guarantees LIFO or FIFO order; a careless RemoveAt(0) on what was meant to be a stack silently breaks the LIFO guarantee. Stack<T> and Queue<T> make that mistake impossible by never exposing the operations that could cause it.

Common Mistakes

Mistake 1 — Calling Pop/Dequeue on an empty collection

Wrong — throws InvalidOperationException:

Stack<int> empty = []; int value = empty.Pop(); // throws — nothing to pop

Correct — check first, or use the Try variant:

if (empty.TryPop(out int result)) Console.WriteLine(result); else Console.WriteLine("Stack is empty.");

Mistake 2 — Using a Stack when you actually need FIFO order (or vice versa)

Using Stack<T> for a print queue would print the most recently submitted job first, leaving earlier jobs waiting indefinitely if new ones keep arriving — the opposite of "first come, first served." Match the type to the ordering guarantee the problem actually needs.

Mistake 3 — Expecting indexed access

Wrong — neither type has an indexer:

Stack<int> s = [1, 2, 3]; int middle = s[1]; // compiler error

If you need to inspect arbitrary positions, you likely want List<T> instead — or you can enumerate a stack/queue with foreach for read-only, order-respecting inspection.

When Should I Use It?

Use Stack<T> when

Use Queue<T> when

Rule of thumb: If the phrase "most recent first" describes your problem, reach for Stack<T>. If "first come, first served" describes it, reach for Queue<T>. If neither ordering rule matters and you just need general storage, List<T> is still the better default.

Mental Model

Stack<T> = LIFO — Push/Pop — a pile of plates
Queue<T> = FIFO — Enqueue/Dequeue — a checkout line

Remember:
· Both wrap a resizable array, just like List<T> — the restriction is deliberate, not a limitation.
· Neither has an indexer — you can only ever reach the "next" item.
· Use TryPop/TryDequeue to avoid exceptions on an empty collection.
· Choosing the wrong one silently flips your processing order — match the type to what the problem actually needs.

Key Takeaway


Check Your Understanding

You've seen how Stack and Queue guarantee opposite retrieval orders. Let's check your understanding.

1. After running Push("A"); Push("B"); Push("C"); on a Stack<string>, what does the first Pop() return?

Show answer

Correct: C

Why C is correct: A stack is Last In, First Out. "C" was pushed most recently, so it's the first one returned by Pop().

Why A is incorrect: "A" was pushed first, which means in a LIFO structure it comes out last, not first.

Why B is incorrect: "B" is in the middle — it will come out on the second Pop(), after "C".

Why D is incorrect: Stack order is fully deterministic and guaranteed — that predictability is the entire reason the type exists.

Reinforcement: LIFO means the most recently added item always comes out first.

2. After running Enqueue("A"); Enqueue("B"); Enqueue("C"); on a Queue<string>, what does the first Dequeue() return?

Show answer

Correct: A

Why A is correct: A queue is First In, First Out. "A" was enqueued first, so it's the first one returned by Dequeue().

Why B is incorrect: "B" was enqueued second, so it comes out on the second Dequeue() call, after "A".

Why C is incorrect: "C" was enqueued last, which in a FIFO structure means it comes out last, not first.

Why D is incorrect: Queue order is based entirely on arrival order — it has nothing to do with the value's content.

Reinforcement: FIFO means the earliest added item always comes out first — the opposite of a stack.

3. A support ticketing system must resolve tickets in the exact order customers submitted them. Which collection is the correct choice, and why?

Show answer

Correct: B

Why B is correct: "Resolve in the order submitted" is a textbook FIFO requirement — the earliest ticket in should be the earliest one out, which is exactly what Queue<T> guarantees.

Why A is incorrect: Stack<T> would resolve the most recently submitted ticket first, leaving older tickets waiting indefinitely if new ones keep arriving — the opposite of what's needed.

Why C is incorrect: The two types produce opposite processing orders — the choice directly determines which tickets get handled first.

Why D is incorrect: A HashSet<T> would guarantee uniqueness, not ordering — it doesn't address the "process in submission order" requirement at all, and it has no Dequeue-style operation.

Reinforcement: "In the order received" is the signature phrase that points to a queue, every time.

4. Why do neither Stack<T> nor Queue<T> provide an indexer like list[2]?

Show answer

Correct: B

Why B is correct: The restricted API is a deliberate design choice, not an oversight. If arbitrary indexing were allowed, code could reach into the middle of a "stack" or "queue" and violate the very ordering guarantee the type exists to provide.

Why A is incorrect: This is intentional design, reinforced consistently across the whole class — not a missing feature.

Why C is incorrect: There's no such size-based restriction anywhere in .NET's collection design.

Why D is incorrect: Neither type exposes any way to read or write an arbitrary position — only Peek (read the next item) and Pop/Dequeue (remove the next item) are available.

Reinforcement: A narrower API is sometimes the feature, not a limitation — it makes incorrect usage impossible to express.

You now know five concrete collection types and when each earns its place. Next, you'll zoom out and see the interfaces — IEnumerable, ICollection, IList, and friends — that tie all of them together.


dotnetmadeeasy.com — Learn C# and .NET, the right way.