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
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
> quitProject Structure
kvstore/
├── kvstore.h — interface
├── kvstore.c — implementation
└── main.c — REPLStep 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);
#endifStep 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
./kvstoreWhat 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 memory —
malloc/freefor each entry, full cleanup inkv_destroy - Struct design — separating the store from the entries
- File I/O — text-format persistence with
fprintf/fgets - Function pointers —
kv_foreachcallback 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?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.