Back to blog
Backend Systemsbeginner

C Project: Key-Value Store

Build an in-memory key-value store in C using hash tables, dynamic memory, and file persistence. A complete systems programming project.

Asma HafeezApril 17, 20266 min read
cprojecthash-tabledata-structuresmemory
Share:𝕏

C Project: Key-Value Store

This project ties together everything from the C series: structs, pointers, dynamic memory, file I/O, and algorithm design. You'll build a hash table-based key-value store.


What We're Building

$ ./kvstore
> set name Alice
OK
> get name
Alice
> set age 30
OK
> get age
30
> delete name
OK
> get name
(nil)
> save store.db
Saved 1 key(s).
> load store.db
Loaded 1 key(s).
> keys
age
> quit

Project Structure

kvstore/
├── kvstore.h   — interface
├── kvstore.c   — implementation
└── main.c      — REPL

Step 1: Header — kvstore.h

C
#ifndef KVSTORE_H
#define KVSTORE_H

#include <stddef.h>

#define KV_CAPACITY  256   // number of hash buckets
#define KV_KEY_MAX    64   // max key length
#define KV_VALUE_MAX 256   // max value length

typedef struct KVEntry {
    char           key[KV_KEY_MAX];
    char           value[KV_VALUE_MAX];
    struct KVEntry* next;   // chaining for collision resolution
} KVEntry;

typedef struct {
    KVEntry* buckets[KV_CAPACITY];
    int      count;
} KVStore;

// Lifecycle
KVStore* kv_create(void);
void     kv_destroy(KVStore* store);

// Operations
int         kv_set(KVStore* store, const char* key, const char* value);
const char* kv_get(const KVStore* store, const char* key);
int         kv_delete(KVStore* store, const char* key);
int         kv_count(const KVStore* store);

// Iteration
void kv_foreach(const KVStore* store,
                void (*callback)(const char* key, const char* value, void* ctx),
                void* ctx);

// Persistence
int kv_save(const KVStore* store, const char* path);
int kv_load(KVStore* store, const char* path);

#endif

Step 2: Implementation — kvstore.c

C
#include "kvstore.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// djb2 hash function
static unsigned int hash(const char* key) {
    unsigned int h = 5381;
    int c;
    while ((c = (unsigned char)*key++)) {
        h = ((h << 5) + h) + c;  // h * 33 + c
    }
    return h % KV_CAPACITY;
}

KVStore* kv_create(void) {
    KVStore* store = calloc(1, sizeof(KVStore));  // zero-initialized
    return store;
}

void kv_destroy(KVStore* store) {
    if (!store) return;
    for (int i = 0; i < KV_CAPACITY; i++) {
        KVEntry* entry = store->buckets[i];
        while (entry) {
            KVEntry* next = entry->next;
            free(entry);
            entry = next;
        }
    }
    free(store);
}

int kv_set(KVStore* store, const char* key, const char* value) {
    if (!store || !key || !value) return -1;
    if (strlen(key) >= KV_KEY_MAX || strlen(value) >= KV_VALUE_MAX) return -1;

    unsigned int idx = hash(key);

    // Check if key already exists
    for (KVEntry* e = store->buckets[idx]; e != NULL; e = e->next) {
        if (strcmp(e->key, key) == 0) {
            strncpy(e->value, value, KV_VALUE_MAX - 1);
            e->value[KV_VALUE_MAX - 1] = '\0';
            return 0;  // updated
        }
    }

    // New entry
    KVEntry* entry = malloc(sizeof(KVEntry));
    if (!entry) return -1;
    strncpy(entry->key, key, KV_KEY_MAX - 1);
    entry->key[KV_KEY_MAX - 1] = '\0';
    strncpy(entry->value, value, KV_VALUE_MAX - 1);
    entry->value[KV_VALUE_MAX - 1] = '\0';

    // Prepend to bucket chain
    entry->next = store->buckets[idx];
    store->buckets[idx] = entry;
    store->count++;
    return 0;
}

const char* kv_get(const KVStore* store, const char* key) {
    if (!store || !key) return NULL;
    unsigned int idx = hash(key);
    for (KVEntry* e = store->buckets[idx]; e != NULL; e = e->next) {
        if (strcmp(e->key, key) == 0) return e->value;
    }
    return NULL;
}

int kv_delete(KVStore* store, const char* key) {
    if (!store || !key) return -1;
    unsigned int idx = hash(key);
    KVEntry* prev = NULL;
    KVEntry* curr = store->buckets[idx];

    while (curr) {
        if (strcmp(curr->key, key) == 0) {
            if (prev) prev->next = curr->next;
            else      store->buckets[idx] = curr->next;
            free(curr);
            store->count--;
            return 0;
        }
        prev = curr;
        curr = curr->next;
    }
    return -1;  // not found
}

int kv_count(const KVStore* store) {
    return store ? store->count : 0;
}

void kv_foreach(const KVStore* store,
                void (*callback)(const char*, const char*, void*),
                void* ctx)
{
    if (!store || !callback) return;
    for (int i = 0; i < KV_CAPACITY; i++) {
        for (KVEntry* e = store->buckets[i]; e != NULL; e = e->next) {
            callback(e->key, e->value, ctx);
        }
    }
}

// Persistence — simple text format: "key=value\n"
int kv_save(const KVStore* store, const char* path) {
    FILE* f = fopen(path, "w");
    if (!f) return -1;

    int saved = 0;
    for (int i = 0; i < KV_CAPACITY; i++) {
        for (KVEntry* e = store->buckets[i]; e != NULL; e = e->next) {
            fprintf(f, "%s=%s\n", e->key, e->value);
            saved++;
        }
    }
    fclose(f);
    return saved;
}

int kv_load(KVStore* store, const char* path) {
    FILE* f = fopen(path, "r");
    if (!f) return -1;

    char line[KV_KEY_MAX + KV_VALUE_MAX + 2];
    int  loaded = 0;

    while (fgets(line, sizeof(line), f)) {
        line[strcspn(line, "\n")] = '\0';  // strip newline
        char* eq = strchr(line, '=');
        if (!eq) continue;
        *eq = '\0';  // split at '='
        const char* key   = line;
        const char* value = eq + 1;
        if (kv_set(store, key, value) == 0) loaded++;
    }
    fclose(f);
    return loaded;
}

Step 3: REPL — main.c

C
#include "kvstore.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAX_INPUT 512

static void print_key(const char* key, const char* value, void* ctx) {
    (void)value; (void)ctx;
    printf("%s\n", key);
}

static void print_entry(const char* key, const char* value, void* ctx) {
    (void)ctx;
    printf("%s = %s\n", key, value);
}

int main(void) {
    KVStore* store = kv_create();
    if (!store) { fprintf(stderr, "Failed to create store\n"); return 1; }

    char input[MAX_INPUT];
    printf("KV Store — type 'help' for commands\n");

    while (1) {
        printf("> ");
        if (!fgets(input, sizeof(input), stdin)) break;
        input[strcspn(input, "\n")] = '\0';

        char cmd[16], arg1[KV_KEY_MAX], arg2[KV_VALUE_MAX];
        int  argc = sscanf(input, "%15s %63s %255s", cmd, arg1, arg2);

        if (argc < 1) continue;

        if (strcmp(cmd, "set") == 0 && argc == 3) {
            if (kv_set(store, arg1, arg2) == 0) printf("OK\n");
            else printf("ERR\n");

        } else if (strcmp(cmd, "get") == 0 && argc >= 2) {
            const char* val = kv_get(store, arg1);
            printf("%s\n", val ? val : "(nil)");

        } else if (strcmp(cmd, "delete") == 0 && argc >= 2) {
            if (kv_delete(store, arg1) == 0) printf("OK\n");
            else printf("(not found)\n");

        } else if (strcmp(cmd, "keys") == 0) {
            kv_foreach(store, print_key, NULL);

        } else if (strcmp(cmd, "all") == 0) {
            kv_foreach(store, print_entry, NULL);

        } else if (strcmp(cmd, "count") == 0) {
            printf("%d\n", kv_count(store));

        } else if (strcmp(cmd, "save") == 0 && argc >= 2) {
            int n = kv_save(store, arg1);
            if (n >= 0) printf("Saved %d key(s).\n", n);
            else printf("Save failed.\n");

        } else if (strcmp(cmd, "load") == 0 && argc >= 2) {
            int n = kv_load(store, arg1);
            if (n >= 0) printf("Loaded %d key(s).\n", n);
            else printf("Load failed.\n");

        } else if (strcmp(cmd, "help") == 0) {
            printf("Commands:\n");
            printf("  set <key> <value>\n");
            printf("  get <key>\n");
            printf("  delete <key>\n");
            printf("  keys\n");
            printf("  all\n");
            printf("  count\n");
            printf("  save <file>\n");
            printf("  load <file>\n");
            printf("  quit\n");

        } else if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
            break;

        } else {
            printf("Unknown command. Type 'help'.\n");
        }
    }

    kv_destroy(store);
    return 0;
}

Building and Running

Bash
gcc -Wall -Wextra -o kvstore kvstore.c main.c
./kvstore

What This Project Demonstrates

  • Hash table — O(1) average lookup using djb2 hash + chaining
  • Linked list — each bucket is a singly-linked list of entries
  • Dynamic memorymalloc/free for each entry, full cleanup in kv_destroy
  • Struct design — separating the store from the entries
  • File I/O — text-format persistence with fprintf/fgets
  • Function pointerskv_foreach callback pattern
  • Header files — clean public interface in .h, implementation in .c

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.