What table-driven design means

Table-driven design replaces sprawling conditionals with structured data. Whenever branching logic keeps growing, move the rules into a table and let the program look up the answer. The approach shines in state machines, permission matrices, pricing rules, and plenty of day-to-day backend tasks.

Here are three lightweight examples that demonstrate the idea.

Example 1: Rock, paper, scissors

Instead of a long if/else chain, keep a victory map and let a lookup drive the full round logic:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

enum Move {
    MOVE_INVALID = 0,
    MOVE_ROCK    = 1,
    MOVE_SCISSORS= 2,
    MOVE_PAPER   = 3
};

static const enum Move WINNING_MOVES[] = {
    MOVE_INVALID,
    MOVE_SCISSORS, // ROCK beats SCISSORS
    MOVE_PAPER,    // SCISSORS beats PAPER
    MOVE_ROCK      // PAPER beats ROCK
};

static const char *MOVE_LABELS[] = {
    "invalid",
    "rock",
    "scissors",
    "paper"
};

static enum Move random_move(void) {
    return (enum Move)(rand() % 3 + 1);
}

static bool player_wins(enum Move player, enum Move opponent) {
    if (player == opponent) {
        return false; // draw
    }
    return WINNING_MOVES[player] == opponent;
}

int main(void) {
    srand((unsigned)time(NULL));

    printf("Choose 1(rock) / 2(scissors) / 3(paper):\n");

    int number = 0;
    if (scanf("%d", &number) != 1 || number < MOVE_ROCK || number > MOVE_PAPER) {
        printf("Invalid input.\n");
        return 1;
    }

    enum Move player   = (enum Move)number;
    enum Move computer = random_move();

    printf("Player: %s\n", MOVE_LABELS[player]);
    printf("Computer: %s\n", MOVE_LABELS[computer]);

    if (player == computer) {
        printf("Result: draw\n");
    } else if (player_wins(player, computer)) {
        printf("Result: player wins\n");
    } else {
        printf("Result: computer wins\n");
    }

    return 0;
}

If new gestures join the game (e.g., “lizard, Spock”), extend the enum and the lookup table and the rest of the program remains untouched.

Example 2: Grade conversion

Mapping scores to letter grades becomes trivial:

char make_grade(uint32_t score) {
    if (score > 100) {
        return 'X';
    }
    static const char GRADES[] = {
        'F','F','F','F','F','F','D','C','B','A','A'
    };
    return GRADES[score / 10];
}

One glance at the array tells you exactly how scores translate to letters.

Example 3: Days per month

Combine leap-year logic with two static arrays:

uint32_t days_in_month(uint32_t year, uint32_t month) {
    static const uint32_t DAYS_COMMON_YEAR[] = {
        31,28,31,30,31,30,31,31,30,31,30,31
    };
    static const uint32_t DAYS_LEAP_YEAR[] = {
        31,29,31,30,31,30,31,31,30,31,30,31
    };
    bool leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    return leap ? DAYS_LEAP_YEAR[month - 1] : DAYS_COMMON_YEAR[month - 1];
}

Now the month logic lives in data, not scattered across conditionals.

Practical lessons

  • Draw the table first: enumerate inputs, outputs, and edge cases before coding.
  • Separate responsibilities: keep lookup logic in one place and let data owners manage the table content.
  • Watch performance: cache large tables or index them to avoid repeated scans.
  • Let non-engineers help: store tables in config files or databases so operations teams can adjust rules safely.

Where it shines

  • Combinatorial conditions turning into an if/else explosion.
  • Stable logic with frequently updated rule content.
  • Systems where business users own the rules.

Final thoughts

Table-driven design is simple yet powerful. By moving decisions into data, you keep code shorter, easier to test, and friendlier for future maintainers.