Build a Request Queue & Cache in C#

Build a Stack, Queue, and Hash Map cache from scratch in C#.

Introduction

30 Second Summary

Every time you load a webpage, your request passes through invisible layers of queues, caches, and history trackers that keep the internet fast and responsive.

In this project, you will build three foundational data structures from scratch in C# and wire them together into a request processing system that simulates how real distributed systems handle traffic.

What You'll Build

A request processing system that queues incoming API requests, caches responses with automatic eviction, and tracks request history using custom-built data structures.

By the end of this project, you'll have:

  • A custom Stack (LIFO) and Queue (FIFO) built from scratch with no built-in collections.
  • A custom Hash Map cache with capacity-based eviction that mirrors how Redis works under the hood.
  • A Request Processor that wires all three structures into a working pipeline, demonstrating cache hits, misses, and FIFO request handling.
  • Secret Mission: Upgrade your cache to use LRU (Least Recently Used) eviction, the same algorithm that powers distributed caches like Redis and Memcached.

Are there any prerequisites?

You should be comfortable with basic C# concepts like arrays, loops, functions, and classes.

No prior knowledge of data structures or system design is needed. This project teaches both from the ground up.

Before We Start

Before We Start...

Set Up Your Project

Every system starts with a foundation. Before you can build Stacks, Queues, and Caches from scratch, you need a working .NET console application that compiles and runs.

In this step, you will scaffold a new C# console project, verify it runs, and set up the code structure that will hold all of your data structures.

In this step, get ready to:
  • Create a new .NET console application.
  • Verify the project compiles and runs.
  • Set up the base code structure for the project.
Create a new console application

The dotnet new console command scaffolds a ready-to-run C# project. You will create this project on your Desktop so it is easy to find later.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your console application by running this command:
dotnet new console -n RequestSystem

What does this command do?

dotnet new console scaffolds a new C# console application with a Program.cs file and a project file.

The -n RequestSystem flag names the project folder and namespace RequestSystem. This keeps everything organized under one name.

  • Navigate into your new project folder by running:
cd RequestSystem
  • Confirm the project files exist by running:
ls
  • You should see Program.cs and RequestSystem.csproj in the output.

Don't see those files?

Make sure you ran the cd RequestSystem command after creating the project. The files live inside the RequestSystem folder, not on the Desktop itself.

If the dotnet command was not recognized, confirm your .NET SDK is installed correctly by running dotnet --version. You should see a version number starting with 9.

Help me troubleshoot my dotnet new console command. I ran the command but I'm not seeing Program.cs or RequestSystem.csproj.

Open the project and verify it runs

Before writing any custom code, confirm the scaffolded project compiles and runs successfully. This ensures your .NET environment is working.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the RequestSystem folder, then click Open.

Using Visual Studio instead?

If you prefer Visual Studio over VS Code, open Visual Studio and select Open a project or solution. Navigate to your Desktop, open the RequestSystem folder, and select the RequestSystem.csproj file.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, below Escape).
  • Run the project by entering this command:
dotnet run
  • You should see Hello, World! printed in the terminal.

What does dotnet run do?

This command compiles your C# code and immediately executes the resulting application. It is the fastest way to test changes during development.

Not seeing Hello, World! output?

Make sure your terminal is in the RequestSystem folder (the one containing RequestSystem.csproj). Run ls to check. If you see the .csproj file listed, you are in the right place.

Help me figure out why dotnet run is not producing output in my RequestSystem project.

Set up the base code structure

The default template uses top-level statements, which means there is no explicit class or Main method visible. For this project, you will use an explicit class structure so each file has a clear namespace and class definition.

  • In the VS Code Explorer sidebar on the left, click Program.cs to open it.
  • Select all the existing content (Cmd+A on macOS, Ctrl+A on Windows) and delete it.
  • Replace it with the following code:
namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
    }
}

What does this code do?

namespace RequestSystem; groups all your classes under the same namespace. Every file you create in this project will share this namespace so they can reference each other.

static void Main(string[] args) is the entry point of your application. All of your testing code will go inside this method as you build each data structure.

  • Save Program.cs by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Run the project again to confirm it still compiles:
dotnet run
  • The terminal should produce no output and no errors. The program runs the empty Main method and exits cleanly.

Seeing a compilation error?

Check that your code has the opening and closing braces matched correctly. The Program class needs its own pair of braces, and the Main method needs its own pair inside the class.

Also confirm you typed namespace RequestSystem; with a semicolon at the end, not a brace. This is the file-scoped namespace syntax in modern C#.

Help me fix a compilation error in my Program.cs file.

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs and that dotnet run produces no errors.

ⓧ I'd like to double check the full code

Here is the complete Program.cs at this point:

namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
    }
}

Your project skeleton is ready. Next up, you will build your first data structure from scratch: a Stack that demonstrates how LIFO ordering works under the hood.

Build Your Stack (LIFO)

Your project compiles and runs. Now it is time to build the first data structure that powers real distributed systems.

A Stack is the simplest structure with the biggest conceptual payoff. It follows LIFO ordering: the last item you put in is the first one that comes out. Think of a stack of plates. You always grab from the top.

In this step, get ready to:
  • Build a generic SimpleStack<T> class from scratch with Push, Pop, Peek, and automatic resizing.
  • Add a PrintAll method to visualize the LIFO ordering.
  • Test the stack in Program.cs to confirm items come out in reverse order.
Create SimpleStack.cs with Push, Pop, Peek, and Resize

Your stack stores items in a plain array. When the array fills up, it doubles in size automatically. This is the same resizing strategy that .NET's built-in collections use under the hood.

  • In VS Code's Explorer sidebar, right-click the RequestSystem folder and select New File.
  • Name the file SimpleStack.cs.
  • Paste the following code into SimpleStack.cs:
namespace RequestSystem;

public class SimpleStack<T>
{
    // Internal array holding the stack's items
    private T[] _items;
    private int _count;

    // Expose the current number of items
    public int Count => _count;

    public SimpleStack(int initialCapacity = 4)
    {
        _items = new T[initialCapacity];
        _count = 0;
    }

    // Add an item to the top of the stack
    public void Push(T item)
    {
        if (_count == _items.Length)
        {
            Resize(_items.Length * 2);
        }
        _items[_count] = item;
        _count++;
    }

    // Remove and return the item on top
    public T Pop()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        _count--;
        T item = _items[_count];
        _items[_count] = default!;
        return item;
    }

    // Return the top item without removing it
    public T Peek()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        return _items[_count - 1];
    }

    // Double the array capacity when full
    private void Resize(int newCapacity)
    {
        T[] newArray = new T[newCapacity];
        for (int i = 0; i < _count; i++)
        {
            newArray[i] = _items[i];
        }
        _items = newArray;
    }
}

What does this code do?

This class manages a generic array that grows as needed. The key members work together to enforce LIFO ordering:

  • Push places a new item at position _count (the current top) and increments the counter. If the array is full, Resize doubles its capacity first.
  • Pop decrements _count, grabs the item at that index, and clears the slot. The last pushed item is always the first popped.
  • Peek returns the top item without modifying anything. It is Pop's read-only sibling.
  • Resize copies all items into a new, larger array. This gives Push an amortized O(1) time complexity.
  • Save SimpleStack.cs.
  • Confirm the file exists by running:
ls

You should see SimpleStack.cs listed alongside Program.cs and RequestSystem.csproj.

Don't see SimpleStack.cs?

  • Make sure you created the file inside the RequestSystem folder (the one containing Program.cs), not the parent folder.
  • Double-check the file name is exactly SimpleStack.cs with a capital S in both words.
  • help me find where my file was created
Add a PrintAll method to visualize LIFO order

You need a way to see all items in the stack from top to bottom. This makes it easy to confirm that LIFO ordering works correctly when you test.

  • In SimpleStack.cs, find the closing brace of the Peek method. Add the following method below it (above the Resize method):
    // Display all items from top (index 0) to bottom
    public void PrintAll()
    {
        for (int i = _count - 1; i >= 0; i--)
        {
            Console.WriteLine($"  [{_count - 1 - i}] {_items[i]}");
        }
    }

What does this code do?

The loop starts at _count - 1 (the top of the stack) and walks down to index 0 (the bottom). The label [0] always represents the top of the stack in the output, so you can visually confirm LIFO ordering.

  • Save SimpleStack.cs.

You will not see output yet. The next substep adds test code to Program.cs that exercises every method.

✔️ Awesome, I've got everything!

Great. Double check you have saved SimpleStack.cs before moving on.

ⓧ I'd like to double check the full code

namespace RequestSystem;

public class SimpleStack<T>
{
    private T[] _items;
    private int _count;

    public int Count => _count;

    public SimpleStack(int initialCapacity = 4)
    {
        _items = new T[initialCapacity];
        _count = 0;
    }

    public void Push(T item)
    {
        if (_count == _items.Length)
        {
            Resize(_items.Length * 2);
        }
        _items[_count] = item;
        _count++;
    }

    public T Pop()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        _count--;
        T item = _items[_count];
        _items[_count] = default!;
        return item;
    }

    public T Peek()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        return _items[_count - 1];
    }

    public void PrintAll()
    {
        for (int i = _count - 1; i >= 0; i--)
        {
            Console.WriteLine($"  [{_count - 1 - i}] {_items[i]}");
        }
    }

    private void Resize(int newCapacity)
    {
        T[] newArray = new T[newCapacity];
        for (int i = 0; i < _count; i++)
        {
            newArray[i] = _items[i];
        }
        _items = newArray;
    }
}
Test your stack in Program.cs

Time to prove LIFO works. You will push three request IDs onto the stack, peek at the top, pop twice, then print what remains.

  • Switch back to Program.cs. Replace the empty Main method body with the following test code:
namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Testing SimpleStack ===");
        // Create a stack and push three request IDs
        var stack = new SimpleStack<string>();
        stack.Push("REQ-001");
        stack.Push("REQ-002");
        stack.Push("REQ-003");
        Console.WriteLine($"Stack count: {stack.Count}");
        Console.WriteLine($"Peek: {stack.Peek()}");
        // Pop returns items in reverse (LIFO) order
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine("Remaining items (top to bottom):");
        stack.PrintAll();
    }
}

What does this test demonstrate?

  • You push REQ-001, REQ-002, then REQ-003. The stack count should be 3.
  • Peek returns REQ-003 (the most recent push) without removing it.
  • The first Pop returns REQ-003, the second returns REQ-002. Last in, first out.
  • PrintAll shows only REQ-001 remains at position [0].
  • Save Program.cs.
  • Run the project by executing:
dotnet run

You should see output confirming LIFO order. Peek shows REQ-003, the two Pops return REQ-003 then REQ-002, and PrintAll shows only REQ-001 left.

Seeing a build error?

  • If you see a "type or namespace 'SimpleStack' could not be found" error, make sure both Program.cs and SimpleStack.cs use the same namespace: namespace RequestSystem;.
  • If you see a syntax error, check that every opening brace { has a matching closing brace } in SimpleStack.cs.
  • Make sure you are running dotnet run from inside the RequestSystem folder (the one with RequestSystem.csproj).
  • help me debug this build error in my stack project

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Testing SimpleStack ===");
        var stack = new SimpleStack<string>();
        stack.Push("REQ-001");
        stack.Push("REQ-002");
        stack.Push("REQ-003");
        Console.WriteLine($"Stack count: {stack.Count}");
        Console.WriteLine($"Peek: {stack.Peek()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine("Remaining items (top to bottom):");
        stack.PrintAll();
    }
}

How does this connect to Solution Architect interviews?

This is how call stacks and undo/redo systems work. In interviews, stacks appear in DFS traversal, expression parsing, and backtracking problems.

When an interviewer asks "how would you implement browser back-button history?" the answer is a stack. The most recently visited page is always the first one you return to.

Your stack is working. Next up, you will build a Queue that processes items in the opposite order. First in, first out.

Build Your Queue (FIFO)

Your Stack processes items in LIFO order. The last item pushed is the first one out.

Now you need the opposite pattern. In real systems, incoming API requests should be processed in the order they arrive. The first request in should be the first request out. This is FIFO (First In, First Out), and the data structure that implements it is a Queue.

In this step, get ready to:
  • Create a SimpleQueue<T> class using a circular buffer design.
  • Implement wrap-around logic and automatic resizing.
  • Test FIFO ordering in your console app.
Create the SimpleQueue class with a circular buffer

A naive queue removes items from the front of an array, which forces every remaining element to shift left. That is O(n) per dequeue. A circular buffer avoids this by using two pointers that wrap around the array boundaries, keeping both enqueue and dequeue at O(1).

  • Create a new file called SimpleQueue.cs in your RequestSystem folder (the same folder that contains SimpleStack.cs).
  • Paste the following code into SimpleQueue.cs:
namespace RequestSystem;

public class SimpleQueue<T>
{
    private T[] _items;
    private int _head;
    private int _tail;
    private int _count;

    public int Count => _count;

    public SimpleQueue(int initialCapacity = 4)
    {
        _items = new T[initialCapacity];
        _head = 0;
        _tail = 0;
        _count = 0;
    }

    public void Enqueue(T item)
    {
        // Double the array size if we've run out of room
        if (_count == _items.Length)
        {
            Resize(_items.Length * 2);
        }
        // Place item at the tail position, then advance tail with wrap-around
        _items[_tail] = item;
        _tail = (_tail + 1) % _items.Length;
        _count++;
    }

    public T Dequeue()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Queue is empty.");
        }
        // Take item from head position, then advance head with wrap-around
        T item = _items[_head];
        _items[_head] = default!;
        _head = (_head + 1) % _items.Length;
        _count--;
        return item;
    }

    public T Peek()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Queue is empty.");
        }
        return _items[_head];
    }

    public void PrintAll()
    {
        // Iterate from head forward, wrapping around if needed
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _items.Length;
            Console.WriteLine($"  [{i}] {_items[index]}");
        }
    }

    private void Resize(int newCapacity)
    {
        // Linearize the circular buffer into a fresh array
        T[] newArray = new T[newCapacity];
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _items.Length;
            newArray[i] = _items[index];
        }
        _items = newArray;
        _head = 0;
        _tail = _count;
    }
}

What does this code do?

This class uses a circular buffer pattern. Instead of shifting elements when you remove from the front, two pointers (_head and _tail) chase each other around the array.

  • Enqueue places an item where _tail points, then advances _tail forward (wrapping to index 0 if it passes the end).
  • Dequeue takes the item at _head and advances _head forward (also wrapping).
  • Resize linearizes the circular buffer into a fresh array, resetting _head to 0 and _tail to _count.
  • Save SimpleQueue.cs.

Seeing red squiggles in your editor?

Make sure the namespace at the top says namespace RequestSystem; exactly. It must match the namespace in Program.cs and SimpleStack.cs.

Also check that SimpleQueue.cs is saved in the same folder as your .csproj file, not in a subfolder.

Help me debug red squiggles in my SimpleQueue.cs file.

Trace how the circular buffer wraps around

The key to this implementation is the modulo operator (%). It makes the pointers loop back to index 0 when they reach the end of the array.

  • Look at the line _tail = (_tail + 1) % _items.Length; in your Enqueue method.
  • Imagine an array of length 4. If _tail is at index 3 and you add 1, the result is (3 + 1) % 4 = 0. The pointer wraps back to the beginning.
  • Now look at the Resize method. When the array is full, items might be scattered (head at index 2, tail at index 1, wrapping around). Resize copies them into a new array in logical order (front to back), resetting _head to 0.

Why a circular buffer instead of shifting elements?

Shifting every element on dequeue costs O(n) time. With a circular buffer, both Enqueue and Dequeue run in O(1) time. The only expensive operation is Resize (O(n)), which happens rarely because the array doubles each time.

This is the same pattern that operating system schedulers use for process queues. In a Solution Architect interview, you would explain that message queues achieve constant-time throughput because they use this circular buffer strategy internally.

Test your queue in Program.cs

Time to confirm that your queue processes items in FIFO order. You will enqueue several requests, dequeue a couple, add one more, and print the remaining items.

  • In Program.cs, find the closing brace of the Main method. Add the following code just above it, after the stack.PrintAll(); line:
        Console.WriteLine("\n=== Testing SimpleQueue ===");
        var queue = new SimpleQueue<string>();
        queue.Enqueue("REQ-A");
        queue.Enqueue("REQ-B");
        queue.Enqueue("REQ-C");
        Console.WriteLine($"Queue count: {queue.Count}");
        Console.WriteLine($"Peek: {queue.Peek()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        queue.Enqueue("REQ-D");
        Console.WriteLine("Remaining items (front to back):");
        queue.PrintAll();

What does this test do?

  • Enqueues three requests (A, B, C) to fill the queue.
  • Peek shows the front item (REQ-A) without removing it.
  • Two Dequeue calls remove REQ-A then REQ-B (FIFO order).
  • Enqueueing REQ-D after dequeueing tests that the circular buffer handles the wrap-around correctly.
  • Save Program.cs.
  • Run your project by executing this command in the terminal:
dotnet run
  • Confirm you see both the Stack tests and the new Queue tests. The Queue output should show Peek: REQ-A, then Dequeue: REQ-A followed by Dequeue: REQ-B. The remaining items should list REQ-C and REQ-D in that order.

Items coming out in the wrong order?

If items come out in reverse (LIFO), check that Dequeue returns from _head (not _tail). The head is the front of the queue.

If you see a build error about SimpleQueue not being found, make sure the namespace in SimpleQueue.cs is RequestSystem and that the file is saved.

Help me debug my SimpleQueue output.

Where do queues appear in system design interviews?

Message queues (RabbitMQ, SQS) use this exact FIFO pattern. In system design interviews, queues appear when you need to decouple producers from consumers or handle traffic spikes.

When an interviewer asks how to handle 10,000 requests per second, the answer starts with "put a queue in front of the processing layer." You just built the data structure that makes that possible.

✔️ Awesome, I've got everything!

Great. Double check you have saved both SimpleQueue.cs and Program.cs.

ⓧ I'd like to double check the full code

Here is the complete SimpleQueue.cs:

namespace RequestSystem;

public class SimpleQueue<T>
{
    private T[] _items;
    private int _head;
    private int _tail;
    private int _count;

    public int Count => _count;

    public SimpleQueue(int initialCapacity = 4)
    {
        _items = new T[initialCapacity];
        _head = 0;
        _tail = 0;
        _count = 0;
    }

    public void Enqueue(T item)
    {
        if (_count == _items.Length)
        {
            Resize(_items.Length * 2);
        }
        _items[_tail] = item;
        _tail = (_tail + 1) % _items.Length;
        _count++;
    }

    public T Dequeue()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Queue is empty.");
        }
        T item = _items[_head];
        _items[_head] = default!;
        _head = (_head + 1) % _items.Length;
        _count--;
        return item;
    }

    public T Peek()
    {
        if (_count == 0)
        {
            throw new InvalidOperationException("Queue is empty.");
        }
        return _items[_head];
    }

    public void PrintAll()
    {
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _items.Length;
            Console.WriteLine($"  [{i}] {_items[index]}");
        }
    }

    private void Resize(int newCapacity)
    {
        T[] newArray = new T[newCapacity];
        for (int i = 0; i < _count; i++)
        {
            int index = (_head + i) % _items.Length;
            newArray[i] = _items[index];
        }
        _items = newArray;
        _head = 0;
        _tail = _count;
    }
}

Here is the complete Program.cs:

namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Testing SimpleStack ===");
        var stack = new SimpleStack<string>();
        stack.Push("REQ-001");
        stack.Push("REQ-002");
        stack.Push("REQ-003");
        Console.WriteLine($"Stack count: {stack.Count}");
        Console.WriteLine($"Peek: {stack.Peek()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine("Remaining items (top to bottom):");
        stack.PrintAll();

        Console.WriteLine("\n=== Testing SimpleQueue ===");
        var queue = new SimpleQueue<string>();
        queue.Enqueue("REQ-A");
        queue.Enqueue("REQ-B");
        queue.Enqueue("REQ-C");
        Console.WriteLine($"Queue count: {queue.Count}");
        Console.WriteLine($"Peek: {queue.Peek()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        queue.Enqueue("REQ-D");
        Console.WriteLine("Remaining items (front to back):");
        queue.PrintAll();
    }
}

Your Queue handles first-in, first-out processing. Next, you will build a Cache using a Hash Map with an eviction policy that kicks out old entries when space runs out.

Build Your Cache (Hash Map with Eviction)

Your Stack handles LIFO history and your Queue handles FIFO processing. The final data structure ties them together: a cache that stores responses so your system never processes the same request twice.

A cache is a Hash Map with a twist. It gives you O(1) lookups by key, but it also has limited capacity. When the cache is full, it must decide which entry to remove. This decision is called an eviction policy. In this step, you will build a cache from scratch using separate chaining for collision handling and a FIFO eviction strategy.

In this step, get ready to:
  • Build a generic hash map with linked-list buckets for collision handling.
  • Implement capacity-based eviction that removes the oldest entry when the cache is full.
  • Test your cache and observe eviction in action.
Create the cache class structure

A hash map stores key-value pairs in an array of "buckets." Each key gets hashed to determine which bucket it belongs to. When two keys hash to the same bucket (a collision), you chain them together using a linked list.

  • Create a new file called SimpleCache.cs in your RequestSystem folder.
  • Add the class skeleton with the nested Entry class, fields, and constructor by pasting this code:
namespace RequestSystem;

public class SimpleCache<TKey, TValue> where TKey : notnull
{
    // Each entry in the hash map stores a key, value, insertion order, and a link to the next entry
    private class Entry
    {
        public TKey Key;
        public TValue Value;
        public int InsertionOrder;
        public Entry? Next;

        public Entry(TKey key, TValue value, int order)
        {
            Key = key;
            Value = value;
            InsertionOrder = order;
            Next = null;
        }
    }

    // The bucket array holds linked-list chains of entries
    private Entry?[] _buckets;
    private int _capacity;
    private int _count;
    private int _insertionCounter;

    public int Count => _count;

    public SimpleCache(int capacity)
    {
        _capacity = capacity;
        // Use 2x capacity for buckets to reduce collisions
        _buckets = new Entry?[capacity * 2];
        _count = 0;
        _insertionCounter = 0;
    }
}

What does this code do?

The nested Entry class holds each cached item. The Next pointer chains entries together when two keys land in the same bucket (separate chaining).

The _buckets array is sized at 2x the capacity to spread entries out and reduce collisions. _insertionCounter tracks the order items were added so we know which one to evict first.

  • Save SimpleCache.cs.

The file won't do much yet. Next, you will add the methods that read and write to the cache.

  • Add the Put method inside your SimpleCache class, after the constructor:
    public void Put(TKey key, TValue value)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        // Check if the key already exists - if so, update the value
        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                current.Value = value;
                return;
            }
            current = current.Next;
        }

        // If cache is full, evict the oldest entry before adding
        if (_count >= _capacity)
        {
            EvictOldest();
        }

        // Insert new entry at the front of the bucket's linked list
        _insertionCounter++;
        Entry newEntry = new Entry(key, value, _insertionCounter);
        newEntry.Next = _buckets[bucketIndex];
        _buckets[bucketIndex] = newEntry;
        _count++;
    }

What does this code do?

  • GetBucketIndex(key) hashes the key to find which bucket it belongs in. You will add this helper method shortly.
  • The while loop walks the linked list at that bucket to check if the key already exists. If it does, the value gets updated in place.
  • If the cache is at capacity, EvictOldest() removes the oldest entry before inserting the new one. You will add this method in the next substep.
  • New entries are prepended to the front of the bucket's chain. This is O(1) insertion.
  • Add the Get and Contains methods after the Put method:
    public TValue Get(TKey key)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        // Walk the linked list at this bucket to find the key
        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                return current.Value;
            }
            current = current.Next;
        }

        throw new KeyNotFoundException($"Key '{key}' not found in cache.");
    }

    public bool Contains(TKey key)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        // Same traversal as Get, but returns true/false instead of the value
        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                return true;
            }
            current = current.Next;
        }

        return false;
    }

What does this code do?

Both methods follow the same pattern: hash the key to find the bucket, then walk the linked list until you find a matching key.

Get throws a KeyNotFoundException if the key is not present. Contains returns a boolean so you can check before calling Get. This is the same pattern .NET's built-in Dictionary uses.

  • Save SimpleCache.cs. The file will not compile yet because GetBucketIndex and EvictOldest do not exist yet.

Seeing red squiggles in your editor?

That is expected. Your editor highlights GetBucketIndex and EvictOldest as missing because you have not added them yet. You will fix this in the next substep.

Help me understand why my SimpleCache.cs has errors right now.

Implement capacity-based eviction

When a cache is full, it needs to decide which entry to throw away. Your cache uses FIFO eviction: the entry that was inserted first gets removed first. This is the simplest eviction policy and maps directly to how you think about expiring old data.

  • Add the PrintContents method after the Contains method. Then add the EvictOldest method directly after it:
    public void PrintContents()
    {
        for (int i = 0; i < _buckets.Length; i++)
        {
            Entry? current = _buckets[i];
            while (current != null)
            {
                Console.WriteLine($"  {current.Key} => {current.Value} (order: {current.InsertionOrder})");
                current = current.Next;
            }
        }
    }

    private void EvictOldest()
    {
        // Scan all buckets to find the entry with the lowest insertion order
        int oldestOrder = int.MaxValue;
        TKey? oldestKey = default;
        int oldestBucket = -1;

        for (int i = 0; i < _buckets.Length; i++)
        {
            Entry? current = _buckets[i];
            while (current != null)
            {
                if (current.InsertionOrder < oldestOrder)
                {
                    oldestOrder = current.InsertionOrder;
                    oldestKey = current.Key;
                    oldestBucket = i;
                }
                current = current.Next;
            }
        }

        if (oldestBucket >= 0 && oldestKey != null)
        {
            Console.WriteLine($"  [EVICTION] Removing '{oldestKey}' from cache (oldest entry)");
            RemoveFromBucket(oldestBucket, oldestKey);
            _count--;
        }
    }

What does this code do?

  • PrintContents iterates every bucket and prints all cached entries. You will use this to inspect the cache state during testing.
  • EvictOldest scans every entry across all buckets to find the one with the lowest InsertionOrder. That entry was inserted first, so it gets removed.
  • The eviction message prints to the console so you can see exactly when and what gets evicted during testing.
  • Add the RemoveFromBucket and GetBucketIndex private helper methods after EvictOldest:
    private void RemoveFromBucket(int bucketIndex, TKey key)
    {
        Entry? current = _buckets[bucketIndex];
        Entry? previous = null;

        // Walk the chain until we find the matching key
        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                // Unlink the node from the chain
                if (previous == null)
                {
                    _buckets[bucketIndex] = current.Next;
                }
                else
                {
                    previous.Next = current.Next;
                }
                return;
            }
            previous = current;
            current = current.Next;
        }
    }

    private int GetBucketIndex(TKey key)
    {
        int hash = key.GetHashCode();
        return Math.Abs(hash) % _buckets.Length;
    }

What does this code do?

  • RemoveFromBucket is classic linked-list deletion. It walks the chain to find the target node, then "unlinks" it by pointing the previous node's Next past the removed entry.
  • GetBucketIndex hashes the key and maps it into the bucket array range using modulo. This is the same approach .NET's Dictionary uses internally.
  • Save SimpleCache.cs. The red squiggles from earlier should now disappear because all referenced methods exist.

Still seeing errors after saving?

  • Make sure every method is inside the SimpleCache class braces, not after the closing }.
  • Check that the namespace at the top says namespace RequestSystem; and matches your other files.
  • Confirm your class declaration includes where TKey : notnull after the generic type parameters.

Help me debug my SimpleCache.cs file.

✔️ Awesome, I've got everything!

Great. Double check you have saved SimpleCache.cs and that there are no red squiggles in the file.

ⓧ I'd like to double check the full code

Here is the complete SimpleCache.cs file. Compare it with yours to find any differences.

namespace RequestSystem;

public class SimpleCache<TKey, TValue> where TKey : notnull
{
    private class Entry
    {
        public TKey Key;
        public TValue Value;
        public int InsertionOrder;
        public Entry? Next;

        public Entry(TKey key, TValue value, int order)
        {
            Key = key;
            Value = value;
            InsertionOrder = order;
            Next = null;
        }
    }

    private Entry?[] _buckets;
    private int _capacity;
    private int _count;
    private int _insertionCounter;

    public int Count => _count;

    public SimpleCache(int capacity)
    {
        _capacity = capacity;
        _buckets = new Entry?[capacity * 2];
        _count = 0;
        _insertionCounter = 0;
    }

    public void Put(TKey key, TValue value)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                current.Value = value;
                return;
            }
            current = current.Next;
        }

        if (_count >= _capacity)
        {
            EvictOldest();
        }

        _insertionCounter++;
        Entry newEntry = new Entry(key, value, _insertionCounter);
        newEntry.Next = _buckets[bucketIndex];
        _buckets[bucketIndex] = newEntry;
        _count++;
    }

    public TValue Get(TKey key)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                return current.Value;
            }
            current = current.Next;
        }

        throw new KeyNotFoundException($"Key '{key}' not found in cache.");
    }

    public bool Contains(TKey key)
    {
        int bucketIndex = GetBucketIndex(key);
        Entry? current = _buckets[bucketIndex];

        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                return true;
            }
            current = current.Next;
        }

        return false;
    }

    public void PrintContents()
    {
        for (int i = 0; i < _buckets.Length; i++)
        {
            Entry? current = _buckets[i];
            while (current != null)
            {
                Console.WriteLine($"  {current.Key} => {current.Value} (order: {current.InsertionOrder})");
                current = current.Next;
            }
        }
    }

    private void EvictOldest()
    {
        int oldestOrder = int.MaxValue;
        TKey? oldestKey = default;
        int oldestBucket = -1;

        for (int i = 0; i < _buckets.Length; i++)
        {
            Entry? current = _buckets[i];
            while (current != null)
            {
                if (current.InsertionOrder < oldestOrder)
                {
                    oldestOrder = current.InsertionOrder;
                    oldestKey = current.Key;
                    oldestBucket = i;
                }
                current = current.Next;
            }
        }

        if (oldestBucket >= 0 && oldestKey != null)
        {
            Console.WriteLine($"  [EVICTION] Removing '{oldestKey}' from cache (oldest entry)");
            RemoveFromBucket(oldestBucket, oldestKey);
            _count--;
        }
    }

    private void RemoveFromBucket(int bucketIndex, TKey key)
    {
        Entry? current = _buckets[bucketIndex];
        Entry? previous = null;

        while (current != null)
        {
            if (current.Key.Equals(key))
            {
                if (previous == null)
                {
                    _buckets[bucketIndex] = current.Next;
                }
                else
                {
                    previous.Next = current.Next;
                }
                return;
            }
            previous = current;
            current = current.Next;
        }
    }

    private int GetBucketIndex(TKey key)
    {
        int hash = key.GetHashCode();
        return Math.Abs(hash) % _buckets.Length;
    }
}
Test your cache and observe eviction

Time to put your cache under pressure. You will create a cache with capacity 3, fill it up, then add a fourth item to trigger eviction.

  • In Program.cs, find the last line of your queue test section (the line that says queue.PrintAll();).
  • Add the following cache test code below that line:
        Console.WriteLine("\n=== Testing SimpleCache ===");
        var cache = new SimpleCache<string, string>(3);
        cache.Put("user:1", "Alice");
        cache.Put("user:2", "Bob");
        cache.Put("user:3", "Charlie");
        Console.WriteLine($"Get user:1 = {cache.Get("user:1")}");
        // This fourth Put triggers eviction of the oldest entry
        cache.Put("user:4", "Diana");
        Console.WriteLine($"Contains user:2? {cache.Contains("user:2")}");
        Console.WriteLine($"Contains user:1? {cache.Contains("user:1")}");

What does this code do?

  • Creates a cache with capacity 3 and fills it with three entries (user:1 through user:3).
  • Retrieves user:1 to confirm it exists before eviction.
  • Adds user:4 which forces the cache to evict user:1 (the oldest entry). Then checks which entries remain.
  • Save Program.cs.
  • Run your project by running this command:
dotnet run
  • In the cache test output, confirm you see the eviction message [EVICTION] Removing 'user:1' from cache (oldest entry).
  • Confirm that Contains user:2? prints True and Contains user:1? prints False (because it was evicted).

Not seeing the eviction message?

  • Make sure you passed 3 as the capacity when creating the cache (new SimpleCache<string, string>(3)).
  • Check that your Put method includes the if (_count >= _capacity) check that calls EvictOldest().
  • Verify that the Get call for user:1 happens before user:4 is added. If it comes after, it will throw an exception because the key no longer exists.

Help me debug my cache eviction output.

How does this connect to system design interviews?

In system design interviews, you discuss cache sizing, eviction policies (LRU, LFU, FIFO), and TTL. The cache you just built uses FIFO eviction, which is the simplest policy.

Redis uses a Hash Map internally with LRU eviction. When an interviewer asks "How would you design a caching layer?", you can now speak from hands-on understanding of buckets, collisions, and eviction.

✔️ Awesome, I've got everything!

Great. Double check you have saved both SimpleCache.cs and Program.cs.

ⓧ I'd like to double check the full code

Here is the complete Program.cs as it should look at the end of this step.

namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Testing SimpleStack ===");
        var stack = new SimpleStack<string>();
        stack.Push("REQ-001");
        stack.Push("REQ-002");
        stack.Push("REQ-003");
        Console.WriteLine($"Stack count: {stack.Count}");
        Console.WriteLine($"Peek: {stack.Peek()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine("Remaining items (top to bottom):");
        stack.PrintAll();

        Console.WriteLine("\n=== Testing SimpleQueue ===");
        var queue = new SimpleQueue<string>();
        queue.Enqueue("REQ-A");
        queue.Enqueue("REQ-B");
        queue.Enqueue("REQ-C");
        Console.WriteLine($"Queue count: {queue.Count}");
        Console.WriteLine($"Peek: {queue.Peek()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        queue.Enqueue("REQ-D");
        Console.WriteLine("Remaining items (front to back):");
        queue.PrintAll();

        Console.WriteLine("\n=== Testing SimpleCache ===");
        var cache = new SimpleCache<string, string>(3);
        cache.Put("user:1", "Alice");
        cache.Put("user:2", "Bob");
        cache.Put("user:3", "Charlie");
        Console.WriteLine($"Get user:1 = {cache.Get("user:1")}");
        cache.Put("user:4", "Diana");
        Console.WriteLine($"Contains user:2? {cache.Contains("user:2")}");
        Console.WriteLine($"Contains user:1? {cache.Contains("user:1")}");
    }
}

You now have all three data structures built and tested. Next, you will wire them together into a request processing pipeline that simulates how real distributed systems handle traffic.

Wire It All Together

You have three working data structures: a Stack for LIFO access, a Queue for FIFO processing, and a Cache with eviction. Individually they solve focused problems. Together they form the backbone of a real request processing system.

In this step, you will build a RequestProcessor class that wires all three structures into a pipeline. Requests arrive in a queue, get processed with cache lookups, and their history is tracked on a stack. This is the exact flow that services like API gateways and load balancers use in production.

In this step, get ready to:
  • Create a RequestProcessor class that uses your Queue, Cache, and Stack together.
  • Simulate a burst of 10 API requests with repeated endpoints to trigger cache hits.
  • Run the full pipeline and observe queuing, caching, eviction, and history in action.
Create the request processor

The RequestProcessor class ties your three data structures into a single workflow. Incoming requests enter the queue, the processor checks the cache before doing "expensive" work, and every processed request gets pushed onto the history stack.

  • Create a new file called RequestProcessor.cs in your RequestSystem folder.
  • Add the following code to build the complete request processing class:
namespace RequestSystem;

public class RequestProcessor
{
    // The three data structures working together
    private SimpleQueue<string> _requestQueue;
    private SimpleCache<string, string> _responseCache;
    private SimpleStack<string> _history;
    private int _cacheHits;
    private int _cacheMisses;

    public RequestProcessor(int cacheCapacity)
    {
        _requestQueue = new SimpleQueue<string>();
        _responseCache = new SimpleCache<string, string>(cacheCapacity);
        _history = new SimpleStack<string>();
        _cacheHits = 0;
        _cacheMisses = 0;
    }

    // Accept a request into the queue (FIFO intake)
    public void SubmitRequest(string endpoint)
    {
        _requestQueue.Enqueue(endpoint);
        Console.WriteLine($"[QUEUED] {endpoint} (queue size: {_requestQueue.Count})");
    }

    // Process all queued requests one by one
    public void ProcessAllRequests()
    {
        Console.WriteLine("\n--- Processing Requests ---");
        while (_requestQueue.Count > 0)
        {
            string endpoint = _requestQueue.Dequeue();
            ProcessSingleRequest(endpoint);
        }
        Console.WriteLine("--- All Requests Processed ---");
    }

    // Check cache first, then process if needed
    private void ProcessSingleRequest(string endpoint)
    {
        if (_responseCache.Contains(endpoint))
        {
            string cached = _responseCache.Get(endpoint);
            Console.WriteLine($"[CACHE HIT]  {endpoint} => {cached}");
            _cacheHits++;
        }
        else
        {
            string response = SimulateProcessing(endpoint);
            _responseCache.Put(endpoint, response);
            Console.WriteLine($"[CACHE MISS] {endpoint} => {response} (now cached)");
            _cacheMisses++;
        }

        // Push every processed request onto the history stack
        _history.Push(endpoint);
    }

    // Simulate an expensive operation (database call, API request, etc.)
    private string SimulateProcessing(string endpoint)
    {
        return $"Response for {endpoint} at {DateTime.Now:HH:mm:ss}";
    }

    // Display request history from the stack (most recent first)
    public void PrintHistory()
    {
        _history.PrintAll();
    }

    // Display cache performance stats
    public void PrintCacheStats()
    {
        Console.WriteLine($"  Cache hits: {_cacheHits}");
        Console.WriteLine($"  Cache misses: {_cacheMisses}");
        Console.WriteLine($"  Hit rate: {(_cacheHits + _cacheMisses > 0 ? (double)_cacheHits / (_cacheHits + _cacheMisses) * 100 : 0):F1}%");
        Console.WriteLine("  Cached entries:");
        _responseCache.PrintContents();
    }
}

What does this class do?

  • _requestQueue holds incoming requests in FIFO order, just like a message queue (SQS, RabbitMQ) buffers traffic.
  • _responseCache stores previously computed responses. If the same endpoint comes in again, the processor skips the expensive work.
  • _history records every processed request with the most recent on top (LIFO), useful for auditing and debugging.
  • ProcessSingleRequest checks the cache first (O(1) lookup), and only calls SimulateProcessing on a cache miss. This is the read-through caching pattern.
  • Save RequestProcessor.cs.

You won't see a change yet. The next chunk connects this class to Program.cs so you can run the full simulation.

Seeing red squiggles in the file?

Make sure the namespace is RequestSystem (matching your other files). If VS Code flags SimpleQueue, SimpleCache, or SimpleStack as unknown, check that those files are saved and share the same namespace.

help me fix namespace errors in my RequestProcessor.cs

Simulate a burst of API requests

Real systems receive repeated requests for the same resources. You will simulate 10 incoming API requests where several endpoints repeat, so you can watch cache hits and evictions in action.

  • In Program.cs, scroll to the bottom of your Main method and find this line:
        Console.WriteLine($"Contains user:1? {cache.Contains("user:1")}");
  • Add the following code below that line to create and run the request processor simulation:
        Console.WriteLine("\n=== Request Processor Simulation ===");
        var processor = new RequestProcessor(cacheCapacity: 3);

        // 10 requests with repeating endpoints to trigger cache hits
        string[] requests = { "/api/users/1", "/api/users/2", "/api/users/3",
                              "/api/users/1", "/api/users/4", "/api/users/2",
                              "/api/users/1", "/api/users/5", "/api/users/3",
                              "/api/users/1" };

        foreach (var request in requests)
        {
            processor.SubmitRequest(request);
        }

        processor.ProcessAllRequests();

        Console.WriteLine("\n=== Request History (most recent first) ===");
        processor.PrintHistory();

        Console.WriteLine("\n=== Cache Contents ===");
        processor.PrintCacheStats();

What does this simulation do?

  • The cache capacity is set to 3, meaning only 3 responses can be stored at once. When a 4th unique endpoint arrives, the oldest cached entry gets evicted.
  • Endpoints like /api/users/1 appear multiple times. The first hit is a cache miss (expensive processing). Subsequent hits return the cached response instantly.
  • After processing, PrintHistory shows requests in reverse chronological order (stack). PrintCacheStats shows the hit rate and what remains cached.
  • Save Program.cs.

Seeing a compile error about RequestProcessor?

Make sure RequestProcessor.cs is saved and uses the namespace RequestSystem; declaration at the top. Both files must share the same namespace for the compiler to find the class.

help me fix my RequestProcessor compile error

✔️ Awesome, I've got everything!

Great. Double check you have saved both RequestProcessor.cs and Program.cs.

ⓧ I'd like to double check the full code

Here is the full RequestProcessor.cs:

namespace RequestSystem;

public class RequestProcessor
{
    private SimpleQueue<string> _requestQueue;
    private SimpleCache<string, string> _responseCache;
    private SimpleStack<string> _history;
    private int _cacheHits;
    private int _cacheMisses;

    public RequestProcessor(int cacheCapacity)
    {
        _requestQueue = new SimpleQueue<string>();
        _responseCache = new SimpleCache<string, string>(cacheCapacity);
        _history = new SimpleStack<string>();
        _cacheHits = 0;
        _cacheMisses = 0;
    }

    public void SubmitRequest(string endpoint)
    {
        _requestQueue.Enqueue(endpoint);
        Console.WriteLine($"[QUEUED] {endpoint} (queue size: {_requestQueue.Count})");
    }

    public void ProcessAllRequests()
    {
        Console.WriteLine("\n--- Processing Requests ---");
        while (_requestQueue.Count > 0)
        {
            string endpoint = _requestQueue.Dequeue();
            ProcessSingleRequest(endpoint);
        }
        Console.WriteLine("--- All Requests Processed ---");
    }

    private void ProcessSingleRequest(string endpoint)
    {
        if (_responseCache.Contains(endpoint))
        {
            string cached = _responseCache.Get(endpoint);
            Console.WriteLine($"[CACHE HIT]  {endpoint} => {cached}");
            _cacheHits++;
        }
        else
        {
            string response = SimulateProcessing(endpoint);
            _responseCache.Put(endpoint, response);
            Console.WriteLine($"[CACHE MISS] {endpoint} => {response} (now cached)");
            _cacheMisses++;
        }

        _history.Push(endpoint);
    }

    private string SimulateProcessing(string endpoint)
    {
        return $"Response for {endpoint} at {DateTime.Now:HH:mm:ss}";
    }

    public void PrintHistory()
    {
        _history.PrintAll();
    }

    public void PrintCacheStats()
    {
        Console.WriteLine($"  Cache hits: {_cacheHits}");
        Console.WriteLine($"  Cache misses: {_cacheMisses}");
        Console.WriteLine($"  Hit rate: {(_cacheHits + _cacheMisses > 0 ? (double)_cacheHits / (_cacheHits + _cacheMisses) * 100 : 0):F1}%");
        Console.WriteLine("  Cached entries:");
        _responseCache.PrintContents();
    }
}

Here is the full Program.cs:

namespace RequestSystem;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Testing SimpleStack ===");
        var stack = new SimpleStack<string>();
        stack.Push("REQ-001");
        stack.Push("REQ-002");
        stack.Push("REQ-003");
        Console.WriteLine($"Stack count: {stack.Count}");
        Console.WriteLine($"Peek: {stack.Peek()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine($"Pop: {stack.Pop()}");
        Console.WriteLine("Remaining items (top to bottom):");
        stack.PrintAll();

        Console.WriteLine("\n=== Testing SimpleQueue ===");
        var queue = new SimpleQueue<string>();
        queue.Enqueue("REQ-A");
        queue.Enqueue("REQ-B");
        queue.Enqueue("REQ-C");
        Console.WriteLine($"Queue count: {queue.Count}");
        Console.WriteLine($"Peek: {queue.Peek()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        Console.WriteLine($"Dequeue: {queue.Dequeue()}");
        queue.Enqueue("REQ-D");
        Console.WriteLine("Remaining items (front to back):");
        queue.PrintAll();

        Console.WriteLine("\n=== Testing SimpleCache ===");
        var cache = new SimpleCache<string, string>(3);
        cache.Put("user:1", "Alice");
        cache.Put("user:2", "Bob");
        cache.Put("user:3", "Charlie");
        Console.WriteLine($"Get user:1 = {cache.Get("user:1")}");
        cache.Put("user:4", "Diana");
        Console.WriteLine($"Contains user:2? {cache.Contains("user:2")}");
        Console.WriteLine($"Contains user:1? {cache.Contains("user:1")}");

        Console.WriteLine("\n=== Request Processor Simulation ===");
        var processor = new RequestProcessor(cacheCapacity: 3);

        string[] requests = { "/api/users/1", "/api/users/2", "/api/users/3",
                              "/api/users/1", "/api/users/4", "/api/users/2",
                              "/api/users/1", "/api/users/5", "/api/users/3",
                              "/api/users/1" };

        foreach (var request in requests)
        {
            processor.SubmitRequest(request);
        }

        processor.ProcessAllRequests();

        Console.WriteLine("\n=== Request History (most recent first) ===");
        processor.PrintHistory();

        Console.WriteLine("\n=== Cache Contents ===");
        processor.PrintCacheStats();
    }
}
Run the complete pipeline

Everything is connected. Time to watch your three data structures collaborate on a realistic workload.

  • Run the simulation by executing this command in your terminal:
dotnet run

You should see a long output with multiple sections. Look for the Request Processor Simulation heading followed by [QUEUED] lines, then [CACHE HIT] and [CACHE MISS] lines during processing, and finally the request history and cache stats.

What should I see?

During the processing phase, you will see [EVICTION] messages when the cache is full and a new unique endpoint arrives. Because the cache only holds 3 entries, older responses get evicted to make room.

The final cache stats show your hit rate. With 10 requests and several repeats, you should see a mix of hits and misses. The history section lists all 10 processed requests with the most recent at the top (LIFO order from the stack).

How does this connect to system design interviews?

You just built the core of what interviewers call a "request processing pipeline." In a real system design interview, you would describe this same flow at a larger scale.

  • The queue decouples request intake from processing. This is how SQS and RabbitMQ handle traffic spikes without dropping requests.
  • The cache avoids redundant work. This is how Redis and Memcached reduce database load in production.
  • The stack provides an audit trail. This is how logging and tracing systems track what happened most recently.

Not seeing any output?

Make sure you are running dotnet run from inside the RequestSystem folder (where your .csproj file is). If you see a build error, check that all four .cs files are saved and use namespace RequestSystem; at the top.

help me debug my dotnet run output

Secret mission

Upgrade to LRU Eviction

Your current cache evicts the oldest inserted item. Real caches like Redis and Memcached use a smarter approach. Can you upgrade your cache to evict the least recently used item instead?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference or delete them to free up disk space.

Resources you used:

  • The RequestSystem project folder on your Desktop (contains Program.cs, SimpleStack.cs, SimpleQueue.cs, SimpleCache.cs, RequestProcessor.cs, and RequestSystem.csproj).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your data structures or plan to extend the project further.

  • Your RequestSystem folder stays on your Desktop, ready for you to open anytime.
  • You can re-run the project at any time with dotnet run from inside the project folder.

Pause - I'll come back to this later

Since there are no running processes or cloud resources, there is nothing to pause. Your project files sit quietly on disk and consume no memory or CPU.

  • Close VS Code or Visual Studio to free up memory.
  • Your RequestSystem folder will be waiting on your Desktop when you return.

Delete - I don't want to use this again

Remove the project folder entirely to start fresh.

  • Close VS Code or Visual Studio if the project is still open.
  • Delete the RequestSystem folder by running the command for your operating system below.

macOS

rm -rf ~/Desktop/RequestSystem

Windows

rmdir /s /q %USERPROFILE%\Desktop\RequestSystem
  • Confirm deletion by running ls ~/Desktop (macOS) or dir %USERPROFILE%\Desktop (Windows). You should no longer see RequestSystem in the output.

Nice Work!

Nice Work!

You just built the three pillars of system design from scratch in C#. Your RequestSystem project simulates how real distributed systems handle traffic using data structures you implemented without relying on System.Collections.Generic.

You've learned how to:

  • Build a Stack (LIFO) from scratch and connect it to real-world patterns like call stacks, undo systems, and DFS traversal.
  • Build a Queue (FIFO) with a circular buffer and connect it to message queues, task processing, and BFS traversal.
  • Build a Cache (Hash Map with eviction) and wire all three structures into a Request Processor simulating real system design patterns like cache hits, misses, and request history.
  • Upgrade your cache to use LRU eviction, the same algorithm that powers Redis and distributed caches.

You now have the foundational vocabulary to discuss queuing, caching, and data flow in Solution Architect interviews. When an interviewer asks how you would design a caching layer, you can speak from hands-on understanding of what happens under the hood.

Ready to quiz yourself?