How I rewrote Redis in less than 70 lines with Golang

Published on May 16, 2025 in Software engineering  

Redis is at the heart of any good bloatware. You have probably heard many times: “We used Redis to manage concurrent access” or other explanations of this kind.

In fact, in 9 out of 10 cases, applications that use Redis don’t need it. Not only do they not need it, but they would gain in simplicity and performance by not using it.

You also have also probably heard that managing concurrent access to data is a complicated task, and it’s better to not reinvent the wheel.

So, let’s see what it really is. In this example, I’ll show you how to use a Golang map to store data with a limited lifetime, just like you might do in Redis.

We will also use an RWMutex to allow read access by several goroutines at the same time. Of course, this code is concurent safe and only one write access is possible at any given time.

Redis is a tool with many features and my title was a bit clickbait. Of course, I’m not going to recreate all of Redis. I’m just going to show you that for most uses it’s very easy to do without it.

Even if it is minimalist, this implementation allows you to store any complex type without serialization. And in modern apps, serialization/deserialization is often the bottleneck that has replaced I/O issues.

I’m not even talking about the performance gain of directly managing everything in memory in the same program.

Here’s the code:

golang
package myredis

import (
	"sync"
	"time"
)

type record struct {
	value      interface{}
	lastAccess int64
	ttl        int64
}

type database struct {
	store map[string]*record
	mu    sync.RWMutex
}

func NewDatabase() *database {
	db := &database{
		store: make(map[string]*record),
	}

	// Start a goroutine to periodically check for expired keys and remove them
	go func() {
		for {
			time.Sleep(time.Second) // Check every second but it can be adjusted
			now := time.Now().Unix()
			db.mu.Lock()
			for key, item := range db.store {
				if now-item.lastAccess > item.ttl {
					delete(db.store, key)
				}
			}
			db.mu.Unlock()
		}
	}()

	return db
}

func (db *database) Set(key string, value interface{}, ttl int64) {
	db.mu.Lock()
	defer db.mu.Unlock()

	db.store[key] = &record{
		value:      value,
		lastAccess: time.Now().Unix(),
		ttl:        ttl,
	}
}

func (db *database) Get(key string) (interface{}, bool) {
	db.mu.RLock()
	defer db.mu.RUnlock()

	if d, ok := db.store[key]; ok {
		d.lastAccess = time.Now().Unix()
		return d.value, true
	}
	return nil, false
}

func (db *database) Delete(key string) {
	db.mu.Lock()
	defer db.mu.Unlock()

	delete(db.store, key)
}

As you can see, this code is extremely simple and does not require extensive explanations.

In the NewDatabase function, an initial size could be specified when allocating the map. If your database has a specific memory requirement, setting it at creation will avoid multiple reallocations and improve performance.

This implementation has few features, but with it, you can easily add all you need, for example the use of wilcard…

And here’s how you can use your new key-value database.

golang
package main

import (
	"time"
	"myproject/myredis"
)

func main() {
	// Create a new Redis-like database
	db := myredis.NewDatabase()

	// Set a key-value pair with an expiration timeout of 1 second
	db.Set("name", "Alice", 1)
	// Set another key-value pair with a longer expiration timeout
	db.Set("age", 30, 5)
	// Note: The values can be of any type, but you will need to use type assertion to retrieve them

	// Get the values
	name, _ := db.Get("name")
	age, _ := db.Get("age")

	// Print the values with type assertion
	println(name.(string)) // Output: Alice
	println(age.(int))     // Output: 30

	// Simulate a 3-second wait
	time.Sleep(3 * time.Second)

	// Try to get an expired key
	if value, ok := db.Get("name"); ok {
		println(value.(string))
	} else {
		println("Key 'name' not found") // Output: Key 'name' not found
	}

	// Try to get a non-expired key
	if value, ok := db.Get("age"); ok {
		println(value.(int)) // Output: 30
	} else {
		println("Key 'age' not found")
	}
}

Of course, Redis is a great tool and sometimes it is necessary to use it. But as you’ve just seen, it is not the ultimate solution to all problems, and it’s often much easier to do without it.

Don’t miss my upcoming posts — hit the follow button on my LinkedIn profile