Build a Real-Time Ad Auction Engine

Build a C++ ad auction engine with hash maps, heaps, and unit tests.

Introduction

30 Second Summary

Every time you search for something online, an auction happens in milliseconds to decide which ads appear at the top of your results. Behind the scenes, advertisers are competing in real time for your attention.

In this project, you will build a simplified version of that ad auction engine in C++. You'll implement the same data structures and algorithms that power systems like Google Ads.

What You'll Build

A command-line ad auction engine that matches search queries to keywords using a hash map for O(1) lookup, then ranks competing ads using a priority queue auction.

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

  • A multi-file C++ project with clean class separation for ads and auction logic.
  • A hash-map keyword index for instant ad retrieval and a max-heap auction that ranks ads by bid and quality score.
  • A Catch2 test suite validating correctness and a benchmarking harness comparing performance with compiler optimizations.
  • Secret Mission: Implement second-price auction pricing where winners pay the second-highest bid plus one cent, just like real ad platforms.

Are there any prerequisites?

You need basic programming experience in any language (variables, functions, loops, conditionals). No prior C++ experience is required.

Set Up Your C++ Development Environment

Before you can build anything, you need a working C++ compiler, an editor that understands your code, and a testing framework ready to go.

In this step, you will verify your compiler, set up VS Code with C++ support, and download the Catch2 testing library into your project folder.

In this step, get ready to:
  • Verify that clang++ is installed and working on your machine.
  • Install the C/C++ extension in VS Code for IntelliSense support.
  • Create your project folder and download the Catch2 testing files.
Verify your C++ compiler

Your Mac comes with clang++ as part of Xcode Command Line Tools. This is the compiler that turns your C++ source code into a runnable program.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your compiler version by running this command:
clang --version

✔️ I see a version number

You should see output that includes Apple clang version followed by a version number. Your compiler is ready to go.

ⓧ Command not found

If you see an error, you need to install Xcode Command Line Tools.

  • Install the tools by running this command:
xcode-select --install

A dialog will appear asking you to confirm the installation. Click Install and wait for the download to finish (this may take several minutes).

  • After installation completes, run clang --version again to confirm it works.

What is clang++?

Clang is the C/C++ compiler that Apple ships with macOS. When you run clang++, it compiles your .cpp source files into an executable binary you can run in Terminal.

Open VS Code and install the C/C++ extension

VS Code gives you syntax highlighting, error detection, and auto-complete for C++. The C/C++ extension powers all of that.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open the Extensions view by pressing Shift+Cmd+X (macOS) or Shift+Ctrl+X (Windows).
  • Type C++ in the search bar at the top of the Extensions panel.
  • Find the extension called C/C++ by Microsoft (it should be the first result).
  • Click Install.

What does the C/C++ extension do?

This extension provides IntelliSense (auto-complete and error highlighting) for your C++ code. It automatically detects clang++ on macOS and configures itself to use C++17 by default.

Don't have VS Code yet?

Download it from https://code.visualstudio.com. Drag the app into your Applications folder, then open it using the search method above.

Create your project folder and download Catch2

Now you will create a dedicated folder for your project and download the Catch2 testing framework files into it. Catch2 uses a two-file "amalgamated" distribution that you can drop directly into any project.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ad-auction-engine
cd ad-auction-engine
  • Download the Catch2 v3.14.0 amalgamated header file by running:
curl -L -o catch_amalgamated.hpp https://github.com/catchorg/Catch2/releases/download/v3.14.0/catch_amalgamated.hpp
  • Download the Catch2 v3.14.0 amalgamated source file by running:
curl -L -o catch_amalgamated.cpp https://github.com/catchorg/Catch2/releases/download/v3.14.0/catch_amalgamated.cpp
  • Confirm the files downloaded successfully by running:
ls

You should see catch_amalgamated.cpp and catch_amalgamated.hpp listed in the output.

Why use the amalgamated distribution?

Catch2 v3 is normally a full library with many header files. The amalgamated distribution bundles everything into just two files: one header and one source file.

This means you can start testing without setting up CMake or a package manager. Just include the header and compile the source alongside your tests.

Download failed or file is empty?

Make sure you have an active internet connection. If curl returns an error, try the command again. You can also check file sizes with ls -lh to verify. The header file should be around 530 KB and the source around 438 KB.

Help me troubleshoot the Catch2 download

  • Open this project folder in VS Code by running:
code .

'code' command not found?

If the code command is not recognized, open VS Code manually, then press Cmd+Shift+P to open the Command Palette. Type Shell Command: Install 'code' command in PATH and select it. Then try code . again.

Help me set up the code command in my terminal

VS Code opens with your ad-auction-engine folder in the Explorer sidebar. You should see catch_amalgamated.cpp and catch_amalgamated.hpp listed there.

Your development environment is ready. Next up, you will define the data structures that represent ads and advertisers in your auction engine.

Define the Data Models

Your development environment is ready. Now it's time to define the data structures that power the auction engine.

Every ad auction system needs two core pieces: a way to represent individual ads, and an engine that organizes those ads for fast retrieval. You'll build both using C++ structs, classes, and the Standard Template Library.

In this step, get ready to:
  • Create the Ad struct that holds advertiser data and computes a combined score.
  • Create the AuctionEngine class with a hash-map-based keyword index.
  • Compile the skeleton to confirm everything links together.
Create the Ad struct

Before the engine can auction anything, it needs a data model for what an ad actually is. In C++, a struct is perfect for bundling related fields together into a single type.

  • Create a new file called ad.hpp in your ad-auction-engine folder.
  • Add the following code to define the Ad struct:
#ifndef AD_HPP
#define AD_HPP

#include <string>

// Represents a single ad with its bidding and quality data
struct Ad {
    std::string advertiser_name;
    std::string keyword;
    double bid_amount;
    double quality_score;
    std::string ad_text;

    // Calculates the ranking score used in the auction
    double combinedScore() const {
        return bid_amount * quality_score;
    }
};

#endif

What does this code do?

The #ifndef / #define / #endif pattern is called a header guard. It prevents the file from being included more than once during compilation, which would cause duplicate definition errors.

The Ad struct bundles five fields that describe a single ad: who placed it, what keyword it targets, how much they bid, a quality score, and the display text. The combinedScore() method multiplies bid by quality to produce the ranking value the auction uses to pick winners.

  • Save ad.hpp.

Why multiply bid by quality?

Real ad platforms don't just award the top spot to the highest bidder. A high bid with terrible quality (irrelevant or misleading ads) hurts the user experience. By multiplying bid amount by quality score, the auction rewards ads that are both well-funded and genuinely useful to the searcher.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

#ifndef AD_HPP
#define AD_HPP

#include <string>

struct Ad {
    std::string advertiser_name;
    std::string keyword;
    double bid_amount;
    double quality_score;
    std::string ad_text;

    double combinedScore() const {
        return bid_amount * quality_score;
    }
};

#endif
Create the AuctionEngine class

The engine needs to store ads in a way that makes retrieval lightning-fast when a search query comes in. A hash map gives you O(1) average-time lookups by mapping each keyword directly to its list of ads.

In C++, you split a class across two files: a header (.hpp) that declares what the class can do, and an implementation file (.cpp) that defines how it does it.

  • Create a new file called auction_engine.hpp in your ad-auction-engine folder.
  • Add the following class declaration:
#ifndef AUCTION_ENGINE_HPP
#define AUCTION_ENGINE_HPP

#include <string>
#include <vector>
#include <unordered_map>
#include "ad.hpp"

// Manages the keyword index and runs auctions on registered ads
class AuctionEngine {
public:
    void registerAd(const Ad& ad);
    std::vector<Ad> runAuction(const std::string& query, int top_n = 3) const;
    size_t getAdCount() const;
    size_t getKeywordCount() const;

private:
    // Maps each keyword to a list of ads targeting that keyword
    std::unordered_map<std::string, std::vector<Ad>> keyword_index_;
};

#endif

What does this code do?

This header declares the public interface of your auction engine. registerAd() adds an ad to the index. runAuction() takes a search query and returns the top N winning ads.

The private member keyword_index_ is an unordered_map (C++'s built-in hash map). Each key is a keyword string, and each value is a vector of all ads targeting that keyword. This gives O(1) lookup when a query comes in.

  • Save auction_engine.hpp.

Seeing red squiggles in VS Code?

If IntelliSense shows errors on #include <unordered_map>, your C++ standard might not be set to C++17. Open the Command Palette with Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), search for C/C++: Edit Configurations (UI), and set the C++ Standard to c++17.

help me fix IntelliSense red squiggles in my auction_engine.hpp file

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

#ifndef AUCTION_ENGINE_HPP
#define AUCTION_ENGINE_HPP

#include <string>
#include <vector>
#include <unordered_map>
#include "ad.hpp"

class AuctionEngine {
public:
    void registerAd(const Ad& ad);
    std::vector<Ad> runAuction(const std::string& query, int top_n = 3) const;
    size_t getAdCount() const;
    size_t getKeywordCount() const;

private:
    std::unordered_map<std::string, std::vector<Ad>> keyword_index_;
};

#endif

Now create the implementation file that provides the method bodies.

  • Create a new file called auction_engine.cpp in the same folder.
  • Add the following stub implementations:
#include "auction_engine.hpp"
#include <queue>
#include <algorithm>

// Adds an ad to the keyword index for fast lookup
void AuctionEngine::registerAd(const Ad& ad) {
    keyword_index_[ad.keyword].push_back(ad);
}

// Stub - will implement full auction logic in the next step
std::vector<Ad> AuctionEngine::runAuction(const std::string& query, int top_n) const {
    return {};
}

// Counts total ads across all keywords
size_t AuctionEngine::getAdCount() const {
    size_t total = 0;
    for (const auto& pair : keyword_index_) {
        total += pair.second.size();
    }
    return total;
}

// Returns how many unique keywords have ads registered
size_t AuctionEngine::getKeywordCount() const {
    return keyword_index_.size();
}

What does this code do?

The registerAd() method is already fully implemented. It uses the ad's keyword as the map key and appends the ad to that keyword's vector. If the keyword doesn't exist yet, the unordered_map creates a new entry automatically.

The runAuction() method is a stub that returns an empty vector for now. You'll implement the full auction logic with a priority queue in the next step. The helper methods getAdCount() and getKeywordCount() let you verify the engine's state during testing.

  • Save auction_engine.cpp.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

#include "auction_engine.hpp"
#include <queue>
#include <algorithm>

void AuctionEngine::registerAd(const Ad& ad) {
    keyword_index_[ad.keyword].push_back(ad);
}

std::vector<Ad> AuctionEngine::runAuction(const std::string& query, int top_n) const {
    return {};
}

size_t AuctionEngine::getAdCount() const {
    size_t total = 0;
    for (const auto& pair : keyword_index_) {
        total += pair.second.size();
    }
    return total;
}

size_t AuctionEngine::getKeywordCount() const {
    return keyword_index_.size();
}
Compile and verify the skeleton builds

Before writing more logic, you want to confirm that all three files link together without errors. A compilation check now catches typos and missing includes early.

  • In your Terminal, make sure you're inside the ad-auction-engine folder.
  • Compile the implementation file by running this command:
clang++ -std=c++17 -c auction_engine.cpp

What does this command do?

The -std=c++17 flag tells the compiler to use the C++17 standard. Without it, clang++ defaults to C++98, which doesn't support unordered_map or other modern features.

The -c flag compiles without linking. This produces an object file (auction_engine.o) and verifies that all your includes and types resolve correctly. You don't need a main() function yet.

  • If the command succeeds, you'll see no output at all. Silence means success in the compiler world.
  • Confirm the object file was created by running:
ls *.o
  • You should see auction_engine.o in the output.

Seeing a compilation error?

If you see fatal error: 'ad.hpp' file not found, make sure ad.hpp is in the same folder as auction_engine.hpp. The #include "ad.hpp" directive looks for the file relative to the current directory.

If you see errors about unknown types like std::unordered_map, check that you included -std=c++17 in the compile command.

help me debug my C++ compilation error

Your data models are locked in and compiling cleanly. Next up, you'll implement the full auction logic that ranks ads using a priority queue and returns the top winners.

Implement the Auction Logic

Your data models and class skeleton are in place from the previous step. Now it's time to write the core algorithm that actually decides which ads win the auction.

The auction works in two phases: first, look up all ads matching a search query using the hash map (O(1) lookup). Then, rank those candidates by score using a priority queue (a heap-based structure that efficiently surfaces the top results).

In this step, get ready to:
  • Implement the runAuction() method with hash map lookup and priority queue ranking.
  • Create a main.cpp program that registers sample ads and runs a live auction.
  • Compile and verify the engine selects the correct winners.
Understand how registerAd() populates the keyword index

Your registerAd() method is already working from the previous step. It handles the first half of the auction system: getting ads into the index so they can be found instantly.

How does the keyword index work?

The keyword_index_ is an unordered_map that maps each keyword string to a vector of ads targeting that keyword.

When registerAd() is called, it pushes the ad into the vector at keyword_index_[ad.keyword]. This gives you O(1) average lookup time when a search query comes in.

Implement the runAuction() method

The runAuction() method currently returns an empty vector. You'll replace it with the full auction algorithm that finds matching ads and ranks them using a priority queue.

  • In auction_engine.cpp, find the runAuction() method.
  • It currently looks like this:
std::vector<Ad> AuctionEngine::runAuction(const std::string& query, int top_n) const {
    return {};
}
  • Replace the entire runAuction() function with this implementation:
std::vector<Ad> AuctionEngine::runAuction(const std::string& query, int top_n) const {
    // Look up ads matching this query keyword
    auto it = keyword_index_.find(query);
    if (it == keyword_index_.end()) {
        return {};
    }

    // Get all candidate ads for this keyword
    const auto& candidates = it->second;

    // Define a comparator that ranks ads by combined score (bid * quality)
    // Using < means the highest score goes to the top of the queue
    auto comparator = [](const Ad& a, const Ad& b) {
        return a.combinedScore() < b.combinedScore();
    };

    // Build a max-heap priority queue with our custom comparator
    std::priority_queue<Ad, std::vector<Ad>, decltype(comparator)> pq(comparator);

    // Push all candidates into the priority queue
    for (const auto& ad : candidates) {
        pq.push(ad);
    }

    // Pop the top N results from the queue
    std::vector<Ad> results;
    int count = 0;
    while (!pq.empty() && count < top_n) {
        results.push_back(pq.top());
        pq.pop();
        count++;
    }

    return results;
}

What does this code do?

  • keyword_index_.find(query) does an O(1) hash map lookup for all ads targeting the search term.
  • The lambda comparator returns true when a has a lower score than b. This is how priority_queue knows to put the highest-scoring ad at the top.
  • decltype(comparator) passes the lambda's type as a template parameter so the priority queue can use it.
  • The while loop pops the top top_n ads from the queue, giving you the highest-ranked results in order.
  • Save auction_engine.cpp.

You won't see a change yet. The next substep creates a main program to test the auction.

✔️ Awesome, I've got everything!

Great. Double check you've saved auction_engine.cpp before moving on.

ⓧ I'd like to double check the full code

#include "auction_engine.hpp"
#include <queue>
#include <algorithm>

void AuctionEngine::registerAd(const Ad& ad) {
    keyword_index_[ad.keyword].push_back(ad);
}

std::vector<Ad> AuctionEngine::runAuction(const std::string& query, int top_n) const {
    auto it = keyword_index_.find(query);
    if (it == keyword_index_.end()) {
        return {};
    }

    const auto& candidates = it->second;

    auto comparator = [](const Ad& a, const Ad& b) {
        return a.combinedScore() < b.combinedScore();
    };

    std::priority_queue<Ad, std::vector<Ad>, decltype(comparator)> pq(comparator);

    for (const auto& ad : candidates) {
        pq.push(ad);
    }

    std::vector<Ad> results;
    int count = 0;
    while (!pq.empty() && count < top_n) {
        results.push_back(pq.top());
        pq.pop();
        count++;
    }

    return results;
}

size_t AuctionEngine::getAdCount() const {
    size_t total = 0;
    for (const auto& pair : keyword_index_) {
        total += pair.second.size();
    }
    return total;
}

size_t AuctionEngine::getKeywordCount() const {
    return keyword_index_.size();
}
Create main.cpp to run a live auction

To verify the engine works end-to-end, you need a program that registers ads, triggers an auction, and prints the winners.

  • Create a new file called main.cpp in your ad-auction-engine folder.
  • Add the following code to main.cpp:
#include <iostream>
#include <iomanip>
#include "auction_engine.hpp"

int main() {
    AuctionEngine engine;

    // Register 5 advertisers competing for "running shoes"
    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Air Max - Free Shipping"});
    engine.registerAd({"Adidas", "running shoes", 2.20, 0.95, "Adidas Ultraboost - 20% Off"});
    engine.registerAd({"New Balance", "running shoes", 1.80, 0.85, "New Balance Fresh Foam"});
    engine.registerAd({"Asics", "running shoes", 3.00, 0.7, "Asics Gel-Kayano Sale"});
    engine.registerAd({"Brooks", "running shoes", 1.50, 0.92, "Brooks Ghost 15 - Top Rated"});

    // Register 3 advertisers competing for "laptop"
    engine.registerAd({"Apple", "laptop", 5.00, 0.98, "MacBook Pro - Power Meets Beauty"});
    engine.registerAd({"Dell", "laptop", 3.50, 0.88, "Dell XPS 15 - Starting at $999"});
    engine.registerAd({"Lenovo", "laptop", 2.80, 0.91, "ThinkPad X1 Carbon - Business Ready"});

    // Run an auction for "running shoes" and get top 3 results
    std::string query = "running shoes";
    std::cout << "=== Ad Auction Results for: \"" << query << "\" ===" << std::endl;
    std::cout << std::endl;

    auto results = engine.runAuction(query, 3);

    // Print each winning ad with its details
    int rank = 1;
    for (const auto& ad : results) {
        std::cout << "#" << rank << " " << ad.ad_text << std::endl;
        std::cout << "   Advertiser: " << ad.advertiser_name
                  << " | Bid: $" << std::fixed << std::setprecision(2) << ad.bid_amount
                  << " | Quality: " << ad.quality_score
                  << " | Score: " << ad.combinedScore() << std::endl;
        std::cout << std::endl;
        rank++;
    }

    // Print engine stats
    std::cout << "Engine stats: " << engine.getAdCount() << " ads across "
              << engine.getKeywordCount() << " keywords" << std::endl;

    return 0;
}

What does this program do?

  • Registers 8 ads across 2 keywords ("running shoes" and "laptop") with different bid amounts and quality scores.
  • Calls runAuction("running shoes", 3) to find the top 3 ads for that query.
  • Prints each winner's rank, ad text, advertiser name, bid, quality score, and combined score.
  • The combined score (bid * quality) determines ranking. Nike ($2.50 * 0.9 = 2.25) beats Asics ($3.00 * 0.7 = 2.10) because Asics has a lower quality score despite bidding more.
  • Save main.cpp.
  • Compile and run the program by running this command in your terminal:
clang++ -std=c++17 -o auction main.cpp auction_engine.cpp && ./auction

You should see output showing the top 3 winners for "running shoes" ranked by combined score, followed by engine stats showing 8 ads across 2 keywords.

What should I see?

The top 3 results should be Nike (score 2.25), Adidas (score 2.09), and Asics (score 2.10). Notice how the priority queue correctly ranks them in descending order of combined score.

Seeing a compile error?

  • Make sure you're running the command from inside your ad-auction-engine folder.
  • Check that main.cpp includes "auction_engine.hpp" with quotes (not angle brackets).
  • If you see "undefined reference" errors, verify both main.cpp and auction_engine.cpp are listed in the compile command.

Help me debug my auction engine compile error.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full main.cpp:

#include <iostream>
#include <iomanip>
#include "auction_engine.hpp"

int main() {
    AuctionEngine engine;

    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Air Max - Free Shipping"});
    engine.registerAd({"Adidas", "running shoes", 2.20, 0.95, "Adidas Ultraboost - 20% Off"});
    engine.registerAd({"New Balance", "running shoes", 1.80, 0.85, "New Balance Fresh Foam"});
    engine.registerAd({"Asics", "running shoes", 3.00, 0.7, "Asics Gel-Kayano Sale"});
    engine.registerAd({"Brooks", "running shoes", 1.50, 0.92, "Brooks Ghost 15 - Top Rated"});

    engine.registerAd({"Apple", "laptop", 5.00, 0.98, "MacBook Pro - Power Meets Beauty"});
    engine.registerAd({"Dell", "laptop", 3.50, 0.88, "Dell XPS 15 - Starting at $999"});
    engine.registerAd({"Lenovo", "laptop", 2.80, 0.91, "ThinkPad X1 Carbon - Business Ready"});

    std::string query = "running shoes";
    std::cout << "=== Ad Auction Results for: \"" << query << "\" ===" << std::endl;
    std::cout << std::endl;

    auto results = engine.runAuction(query, 3);

    int rank = 1;
    for (const auto& ad : results) {
        std::cout << "#" << rank << " " << ad.ad_text << std::endl;
        std::cout << "   Advertiser: " << ad.advertiser_name
                  << " | Bid: $" << std::fixed << std::setprecision(2) << ad.bid_amount
                  << " | Quality: " << ad.quality_score
                  << " | Score: " << ad.combinedScore() << std::endl;
        std::cout << std::endl;
        rank++;
    }

    std::cout << "Engine stats: " << engine.getAdCount() << " ads across "
              << engine.getKeywordCount() << " keywords" << std::endl;

    return 0;
}

Your auction engine is live and selecting winners. Next up, you'll write a test suite to prove the logic is correct across edge cases.

Write Unit Tests with Catch2

Your auction engine compiles and runs, but how do you know it actually produces the right results?

In this step, you will write automated tests using Catch2 to verify your engine's correctness. Each test creates a fresh AuctionEngine, feeds it specific data, and checks that the output matches expectations.

In this step, get ready to:
  • Write 5 test cases covering registration, auction ranking, and edge cases.
  • Compile and run the test suite to confirm all tests pass.
Create your test file

Unit tests isolate small pieces of your code and verify each one independently. With Catch2, each TEST_CASE is a self-contained function that sets up its own data, runs the code, and checks the result with REQUIRE assertions.

  • Create a new file called tests.cpp in your ad-auction-engine folder.
  • Add the includes and first test case by pasting this code:
#include "catch_amalgamated.hpp"
#include "auction_engine.hpp"

TEST_CASE("Registering ads populates the index", "[register]") {
    AuctionEngine engine;

    REQUIRE(engine.getAdCount() == 0);
    REQUIRE(engine.getKeywordCount() == 0);

    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Ad"});
    REQUIRE(engine.getAdCount() == 1);
    REQUIRE(engine.getKeywordCount() == 1);

    engine.registerAd({"Adidas", "running shoes", 2.20, 0.95, "Adidas Ad"});
    REQUIRE(engine.getAdCount() == 2);
    REQUIRE(engine.getKeywordCount() == 1);

    engine.registerAd({"Apple", "laptop", 5.00, 0.98, "Apple Ad"});
    REQUIRE(engine.getAdCount() == 3);
    REQUIRE(engine.getKeywordCount() == 2);
}

What does this code do?

The two #include lines pull in the Catch2 testing framework and your auction engine code.

The first test case verifies that registerAd() correctly populates the keyword index. After each registration, it checks that getAdCount() increments and getKeywordCount() only increases when a new keyword appears.

  • Next, add two test cases that verify your auction logic picks the correct winners. Paste this code below the first test case:
TEST_CASE("Auction returns correct winner", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"LowBid", "shoes", 1.00, 0.5, "Low Ad"});
    engine.registerAd({"HighBid", "shoes", 5.00, 0.9, "High Ad"});
    engine.registerAd({"MidBid", "shoes", 3.00, 0.8, "Mid Ad"});

    auto results = engine.runAuction("shoes", 1);
    REQUIRE(results.size() == 1);
    REQUIRE(results[0].advertiser_name == "HighBid");
}

TEST_CASE("Auction returns top N results in order", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"A", "query", 1.00, 1.0, "Ad A"});
    engine.registerAd({"B", "query", 3.00, 1.0, "Ad B"});
    engine.registerAd({"C", "query", 2.00, 1.0, "Ad C"});

    auto results = engine.runAuction("query", 3);
    REQUIRE(results.size() == 3);
    REQUIRE(results[0].advertiser_name == "B");
    REQUIRE(results[1].advertiser_name == "C");
    REQUIRE(results[2].advertiser_name == "A");
}

What does this code do?

  • The "correct winner" test registers three ads with different bids and quality scores. HighBid (5.00 * 0.9 = 4.5) should beat MidBid (3.00 * 0.8 = 2.4) and LowBid (1.00 * 0.5 = 0.5).
  • The "top N in order" test uses a quality score of 1.0 for all ads so the ranking depends purely on bid amount. This makes it easy to verify the sort order is B > C > A.
  • Finally, add two edge-case tests at the bottom of the file:
TEST_CASE("Empty query returns no results", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Ad"});

    auto results = engine.runAuction("nonexistent keyword");
    REQUIRE(results.empty());
}

TEST_CASE("Auction with single ad returns that ad", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"Solo", "unique", 1.00, 1.0, "Solo Ad"});

    auto results = engine.runAuction("unique", 3);
    REQUIRE(results.size() == 1);
    REQUIRE(results[0].advertiser_name == "Solo");
}

What does this code do?

  • The "empty query" test verifies that searching for a keyword with no matching ads returns an empty vector instead of crashing.
  • The "single ad" test checks that the auction handles the minimum viable case. Requesting top 3 when only 1 ad exists should return just that one ad.
  • Save tests.cpp.

Why write tests for code that already works?

Your engine runs fine with the sample data in main.cpp, but that only proves one scenario. Automated tests prove your code handles edge cases, unexpected inputs, and future changes without breaking.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

#include "catch_amalgamated.hpp"
#include "auction_engine.hpp"

TEST_CASE("Registering ads populates the index", "[register]") {
    AuctionEngine engine;

    REQUIRE(engine.getAdCount() == 0);
    REQUIRE(engine.getKeywordCount() == 0);

    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Ad"});
    REQUIRE(engine.getAdCount() == 1);
    REQUIRE(engine.getKeywordCount() == 1);

    engine.registerAd({"Adidas", "running shoes", 2.20, 0.95, "Adidas Ad"});
    REQUIRE(engine.getAdCount() == 2);
    REQUIRE(engine.getKeywordCount() == 1);

    engine.registerAd({"Apple", "laptop", 5.00, 0.98, "Apple Ad"});
    REQUIRE(engine.getAdCount() == 3);
    REQUIRE(engine.getKeywordCount() == 2);
}

TEST_CASE("Auction returns correct winner", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"LowBid", "shoes", 1.00, 0.5, "Low Ad"});
    engine.registerAd({"HighBid", "shoes", 5.00, 0.9, "High Ad"});
    engine.registerAd({"MidBid", "shoes", 3.00, 0.8, "Mid Ad"});

    auto results = engine.runAuction("shoes", 1);
    REQUIRE(results.size() == 1);
    REQUIRE(results[0].advertiser_name == "HighBid");
}

TEST_CASE("Auction returns top N results in order", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"A", "query", 1.00, 1.0, "Ad A"});
    engine.registerAd({"B", "query", 3.00, 1.0, "Ad B"});
    engine.registerAd({"C", "query", 2.00, 1.0, "Ad C"});

    auto results = engine.runAuction("query", 3);
    REQUIRE(results.size() == 3);
    REQUIRE(results[0].advertiser_name == "B");
    REQUIRE(results[1].advertiser_name == "C");
    REQUIRE(results[2].advertiser_name == "A");
}

TEST_CASE("Empty query returns no results", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"Nike", "running shoes", 2.50, 0.9, "Nike Ad"});

    auto results = engine.runAuction("nonexistent keyword");
    REQUIRE(results.empty());
}

TEST_CASE("Auction with single ad returns that ad", "[auction]") {
    AuctionEngine engine;
    engine.registerAd({"Solo", "unique", 1.00, 1.0, "Solo Ad"});

    auto results = engine.runAuction("unique", 3);
    REQUIRE(results.size() == 1);
    REQUIRE(results[0].advertiser_name == "Solo");
}
Compile and run the tests

Compiling your test file requires linking three source files together: your engine implementation, the Catch2 framework, and the tests themselves.

  • Compile your test executable by running this command in Terminal:
clang++ -std=c++17 auction_engine.cpp catch_amalgamated.cpp tests.cpp -o tests

What does this command do?

  • clang++ -std=c++17 compiles using the C++17 standard.
  • auction_engine.cpp provides your engine logic, catch_amalgamated.cpp provides the Catch2 test runner, and tests.cpp contains your test cases.
  • -o tests names the output binary tests.
  • Run the test binary by running:
./tests
  • You should see output ending with a line that says all 5 test cases passed with all assertions successful.

Seeing compilation errors?

  • Make sure you are running the command from inside the ad-auction-engine folder where all your files live.
  • Check that catch_amalgamated.hpp and catch_amalgamated.cpp are in the same folder as tests.cpp.
  • If you see an "undeclared identifier" error, confirm your #include lines match the filenames exactly (case-sensitive).

Why aren't my tests compiling?

Tests compiled but some are failing?

  • Catch2 prints which assertion failed and the expected vs actual values. Compare the failing test's data setup against the expected result.
  • Double check your runAuction() comparator uses combinedScore() (bid * quality) and that it sorts from highest to lowest.

Help me debug my failing Catch2 test

All five tests pass. You now have proof that your auction engine handles registration, correct ranking, ordering, empty queries, and single-ad auctions. Next up, you will benchmark the engine's performance and see how compiler optimizations speed things up.

Benchmark and Optimize

Your auction engine works correctly and your Catch2 tests prove it. The next question every systems engineer asks is: how fast is it?

In this step, you'll build a benchmarking harness that measures your engine's throughput across 100,000 auctions. Then you'll see how compiler optimization flags can dramatically speed things up without changing a single line of your code.

In this step, get ready to:
  • Write a benchmarking harness that times 100,000 auction lookups.
  • Compare performance between unoptimized and optimized compilation.
  • Document the speedup results to demonstrate awareness of compiler optimization effects.
Write the benchmarking harness

A benchmark needs to simulate realistic load. You'll register 1,000 ads spread across 100 keywords, then run 100,000 auction queries against them. The std::chrono::steady_clock provides a monotonic timer that's ideal for measuring elapsed time.

  • Create a new file called benchmark.cpp in your ad-auction-engine folder.
  • Paste the following code into benchmark.cpp:
#include <iostream>
#include <chrono>
#include <iomanip>
#include "auction_engine.hpp"

int main() {
    AuctionEngine engine;

    // Register 1000 ads spread across 100 keywords
    for (int i = 0; i < 1000; i++) {
        Ad ad;
        ad.advertiser_name = "Advertiser_" + std::to_string(i);
        ad.keyword = "keyword_" + std::to_string(i % 100);
        ad.bid_amount = 1.0 + (i % 50) * 0.1;
        ad.quality_score = 0.5 + (i % 10) * 0.05;
        ad.ad_text = "Ad text " + std::to_string(i);
        engine.registerAd(ad);
    }

    const int iterations = 100000;

    // Start the timer
    auto start = std::chrono::steady_clock::now();

    // Run 100,000 auction queries
    for (int i = 0; i < iterations; i++) {
        std::string query = "keyword_" + std::to_string(i % 100);
        auto results = engine.runAuction(query, 3);
        // Prevent compiler from optimizing away the work
        if (results.empty()) {
            std::cerr << "unexpected empty result" << std::endl;
        }
    }

    // Stop the timer
    auto end = std::chrono::steady_clock::now();

    // Calculate and print results
    auto total_us = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
    double avg_us = static_cast<double>(total_us) / iterations;

    std::cout << "=== Auction Benchmark Results ===" << std::endl;
    std::cout << "Iterations: " << iterations << std::endl;
    std::cout << "Total time: " << total_us / 1000 << " ms" << std::endl;
    std::cout << "Average per auction: " << std::fixed << std::setprecision(2)
              << avg_us << " us" << std::endl;
    std::cout << std::endl;
    std::cout << "Compare by compiling with different flags:" << std::endl;
    std::cout << "  clang++ -std=c++17 -O0 ... (no optimization)" << std::endl;
    std::cout << "  clang++ -std=c++17 -O2 ... (optimized)" << std::endl;

    return 0;
}

What does this code do?

This benchmark simulates a realistic workload by creating 1,000 ads distributed across 100 unique keywords. Each keyword ends up with about 10 competing ads.

  • The for loop registers ads with varying bids and quality scores using modular arithmetic to create diversity.
  • steady_clock::now() captures the start and end timestamps. A steady clock is guaranteed monotonic, meaning it never jumps backward.
  • The results.empty() check prevents the compiler from optimizing away the auction calls entirely. Without it, an aggressive optimizer might skip the work since the results aren't used.
  • duration_cast<microseconds> converts the raw clock difference into microseconds for human-readable output.
  • Save benchmark.cpp.

Seeing a red squiggly under chrono?

IntelliSense sometimes lags behind when you create a new file. Save the file and wait a moment for the C/C++ extension to parse it.

If the error persists, make sure your #include line says <chrono> with angle brackets, not quotes.

Help me fix IntelliSense errors in my benchmark file.

Compare optimized vs unoptimized compilation

C++ compilers can apply powerful optimizations like inlining functions, unrolling loops, and eliminating dead code. The -O0 flag disables all optimizations (useful for debugging). The -O2 flag enables aggressive optimizations that make production code significantly faster.

  • Compile and run the benchmark without optimizations by running this command:
clang++ -std=c++17 -O0 auction_engine.cpp benchmark.cpp -o benchmark_O0 && ./benchmark_O0
  • Note the "Average per auction" number in the output. This is your baseline.
  • Now compile and run with optimizations enabled by running this command:
clang++ -std=c++17 -O2 auction_engine.cpp benchmark.cpp -o benchmark_O2 && ./benchmark_O2
  • Compare the "Average per auction" number between the two runs. You should see a significant speedup with -O2.

What kind of speedup should I see?

The exact numbers depend on your machine, but a 2x to 5x speedup is typical. The optimizer inlines small functions like combinedScore(), eliminates redundant memory accesses, and rearranges instructions to keep the CPU pipeline full.

This is why production C++ systems are always compiled with optimization flags. The same code, compiled differently, can serve significantly more requests per second.

Both runs show the same time?

Double-check that you used -O0 for the first compile and -O2 for the second. Those are the letter O followed by a number, not zeros.

Also make sure you're running the correct binary. The first command creates benchmark_O0 and the second creates benchmark_O2.

Help me figure out why my benchmark results are the same for both optimization levels.

Document your benchmark results

Recording benchmark results directly in your source code is a common practice for systems code. It makes the performance characteristics visible to anyone reading the file.

  • Open benchmark.cpp in VS Code.
  • Add a comment block at the very top of the file (above the #include lines) documenting your before and after numbers.
  • Include the average microseconds per auction for both -O0 and -O2, plus the speedup factor (divide the -O0 number by the -O2 number).
  • Save benchmark.cpp.

Example comment format

Your comment might look something like: "Benchmark results (MacBook M-series): -O0 avg 4.52 us/auction, -O2 avg 1.23 us/auction, speedup 3.7x". Use your actual numbers from the runs you just did.

Why document performance in the source?

In production systems, performance expectations are part of the codebase. When someone modifies the auction algorithm, they can re-run the benchmark and compare against the documented baseline to catch performance regressions immediately.

✔️ Awesome, I've got everything!

Great. Double check you've saved benchmark.cpp with your results comment at the top.

ⓧ I'd like to double check the full code

Here is the full benchmark.cpp file (without your personalized results comment, which goes at the top):

#include <iostream>
#include <chrono>
#include <iomanip>
#include "auction_engine.hpp"

int main() {
    AuctionEngine engine;

    for (int i = 0; i < 1000; i++) {
        Ad ad;
        ad.advertiser_name = "Advertiser_" + std::to_string(i);
        ad.keyword = "keyword_" + std::to_string(i % 100);
        ad.bid_amount = 1.0 + (i % 50) * 0.1;
        ad.quality_score = 0.5 + (i % 10) * 0.05;
        ad.ad_text = "Ad text " + std::to_string(i);
        engine.registerAd(ad);
    }

    const int iterations = 100000;

    auto start = std::chrono::steady_clock::now();

    for (int i = 0; i < iterations; i++) {
        std::string query = "keyword_" + std::to_string(i % 100);
        auto results = engine.runAuction(query, 3);
        if (results.empty()) {
            std::cerr << "unexpected empty result" << std::endl;
        }
    }

    auto end = std::chrono::steady_clock::now();

    auto total_us = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
    double avg_us = static_cast<double>(total_us) / iterations;

    std::cout << "=== Auction Benchmark Results ===" << std::endl;
    std::cout << "Iterations: " << iterations << std::endl;
    std::cout << "Total time: " << total_us / 1000 << " ms" << std::endl;
    std::cout << "Average per auction: " << std::fixed << std::setprecision(2)
              << avg_us << " us" << std::endl;
    std::cout << std::endl;
    std::cout << "Compare by compiling with different flags:" << std::endl;
    std::cout << "  clang++ -std=c++17 -O0 ... (no optimization)" << std::endl;
    std::cout << "  clang++ -std=c++17 -O2 ... (optimized)" << std::endl;

    return 0;
}

Secret mission

Implement Second-Price Auction Pricing

Real ad platforms like Google Ads don't charge winners their own bid. They use a second-price auction model where the winner pays just one cent more than the second-highest bid. This incentivizes truthful bidding and is a fundamental concept in auction theory. In this mission, you'll extend your engine to calculate and return the actual price each winner pays.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • Project folder on your Desktop (source files, Catch2 files, compiled binaries).
  • VS Code C/C++ extension.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your auction engine or use it as a reference for future C++ projects.

  • Your source files, Catch2 files, and compiled binaries stay on your Desktop.
  • The C/C++ extension remains available in VS Code for any future C++ work.

Pause - I'll come back to this later

Since this project has no running processes or cloud resources, pausing looks the same as keeping. Your files sit on disk without consuming any active resources.

  • No running processes to stop. Everything is already idle.
  • Optionally delete the compiled binaries to save a small amount of disk space. You can recompile from source at any time.

Delete - I don't want to use this again

Remove all project files and extensions to start fresh.

  • Delete your project folder by running this command in Terminal:

macOS

rm -rf ~/Desktop/ad-auction-engine

Windows

rmdir /s /q %USERPROFILE%\Desktop\ad-auction-engine
  • Uninstall the C/C++ extension in VS Code.
  • Open VS Code and press Shift+Cmd+X (macOS) or Shift+Ctrl+X (Windows) to open the Extensions view.
  • Search for C++, click on the C/C++ extension, and click Uninstall.

Nice Work!

Nice Work!

Congratulations! You just built a working ad auction engine from scratch in C++.

You've learned how to:

  • Build a multi-file C++ project with clean OOP design using an Ad struct and AuctionEngine class.
  • Implement O(1) keyword lookup with std::unordered_map and rank ads using a heap-based std::priority_queue with a custom comparator.
  • Write a Catch2 test suite proving correctness, and benchmark performance across compiler optimization levels to demonstrate measurable speedup.
  • Secret Mission: Implement second-price auction pricing where winners pay the second-highest bid plus one cent, matching real-world ad platform behavior.

Ready to quiz yourself?