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.
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.
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.
| Operation | Stack<T> (LIFO) | Queue<T> (FIFO) |
|---|---|---|
| Add an item | Push(item) | Enqueue(item) |
| Remove and return the "next" item | Pop() — most recently pushed | Dequeue() — earliest enqueued |
| Look without removing | Peek() | Peek() |
| Check if empty first (safe) | TryPop(out T) | TryDequeue(out T) |
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.
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 itselfSimilarly, 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."
Stack<string> history = [];
history.Push("Page A");
history.Push("Page B");
history.Push("Page C");
string top = history.Peek(); // "Page C" — still there, just looked at
string popped = history.Pop(); // "Page C" — removed and returned
Peek is read-only; Pop removes as it returns.if (history.TryPop(out string? previous))
Console.WriteLine($"Going back to {previous}");
else
Console.WriteLine("No history to go back to.");
Pop() or Peek() on an empty stack throws InvalidOperationException — TryPop avoids that entirely.Queue<string> printJobs = [];
printJobs.Enqueue("Report.pdf");
printJobs.Enqueue("Invoice.pdf");
printJobs.Enqueue("Photo.png");
string next = printJobs.Peek(); // "Report.pdf" — the earliest one, still there
string printed = printJobs.Dequeue(); // "Report.pdf" — removed and returned
while (printJobs.Count > 0)
{
string job = printJobs.Dequeue();
Console.WriteLine($"Printing {job}...");
}
Count, Dequeue, repeat — is the standard way to process every item in submission order.// 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:
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 Benpublic 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'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.
List<T>, both Stack<T> and Queue<T> use a resizable backing array internally, growing it the same way when it fills up.Push adds to what's conceptually the "top" of the backing array, and Pop removes from that exact same end.Enqueue adds to the "back" of the queue; Dequeue removes from the "front" — two different ends of the same underlying structure.Queue<T> implementation uses a clever technique called a circular buffer: instead of physically shifting every element forward each time something is dequeued, it just moves an internal "front" pointer forward, wrapping back around to the start of the array once it reaches the end. This keeps both Enqueue and Dequeue O(1), without the cost of shifting elements.stack[2] or queue[2] — this is intentional. Restricting access to only the "next" item is precisely what makes the LIFO/FIFO guarantee bulletproof; there's no way to accidentally reach around it.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.
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.
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."); 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.
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.
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.
Push/Pop — a pile of platesEnqueue/Dequeue — a checkout lineList<T> — the restriction is deliberate, not a limitation.TryPop/TryDequeue to avoid exceptions on an empty collection.Push to add, Pop to remove the most recently added item.Enqueue to add, Dequeue to remove the earliest added item.List<T>, but deliberately restrict access to only the "next" item to guarantee correct ordering.TryPop/TryDequeue instead of Pop/Dequeue whenever the collection might be empty.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?
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?
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?
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]?
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.