Getting set up

A Maven project that compiles, runs, and is committed to git.

  • Part 0
  • beginner
  • about 30 minutes, mostly downloads

You will build

a Maven project that compiles, runs, and is committed to git

  • 6 lines

Where we are going

By the end of this series you will have a Redis server the real client cannot tell from the original:

$ redis-cli ping
PONG
$ redis-cli set user:1 "Subash" ex 60
OK
$ redis-cli ttl user:1
(integer) 60
$ redis-cli zadd leaderboard 100 alice 250 bob
(integer) 2
$ redis-cli zrange leaderboard 0 -1 withscores
1) "alice"
2) "100"
3) "bob"
4) "250"

67 commands, six data types, key expiry, transactions, pub/sub, persistence that real Redis can read, and replication between two instances. Roughly 2,500 lines of plain Java.

This post gets your machine ready. No networking yet.

What the series covers

Thirteen posts. Each one ends with a server that does more than it did at the start, and every post is runnable on its own, so you can stop anywhere and still have something that works.

PostYou end up withNew commands
0A project that builds
1A TCP server that echoes bytes
2Framing, and many clients at once
3The Redis protocol, parsed and written
4A command dispatcherPING ECHO
5A key-value store with expirySET GET
6Counters and listsINCR RPUSH LRANGE and 9 more
7Blocking reads and transactionsBLPOP MULTI EXEC WATCH
8Pub/sub and streamsSUBSCRIBE PUBLISH XADD XREAD
9Hashes, sets, sorted sets, pagingHSET SADD ZADD SCAN and 15 more
10Configuration and the rest of the APICONFIG INFO TTL EXPIRE and 13 more
11Data that survives a restartSAVE BGSAVE
12A replica that follows a masterREPLCONF PSYNC WAIT

73 commands at the end, roughly 2,500 lines, 295 tests.

How much of Redis that is

Redis 8.2 has 267 top-level commands. You can check that yourself, which is how I got the number:

$ redis-cli command count
(integer) 267

So the series builds 27 percent of Redis by command count. Where that sits, group by group:

GroupYou buildRedis has
Transactions55
Strings1222
Keys1027
Lists822
Hashes828
Sets517
Sorted sets635
Streams417
Pub/sub39
Replication38
Persistence24
Scripting08
Cluster04
Server, admin, other761

27 percent by count, and much more than that by ideas, because most of what is missing is a new name over machinery you will already have built. ZRANGEBYSCORE reuses the ordering ZRANGE taught you. SINTER is set algebra over a set you already store. BRPOP is BLPOP from the other end.

The parts that would genuinely teach something new are Lua scripting, which needs an embedded interpreter, and consumer groups, which turn a stream into a work queue. Both are named in the roadmap, along with every simplification made along the way and what each remaining piece would cost.

How each post is laid out

Every stage inside a post follows the same shape, so you always know where you are.

The idea            what the problem is, with any new terms defined
The code            complete, not a fragment
Run it              exact commands and exact expected output
Notice that         what you should have seen, and why it matters
Try it yourself     small experiments, including one that breaks on purpose
What usually goes wrong   symptoms first, including the bugs I hit
Go deeper           optional, links to specs and Javadoc

The bugs stay in. When something took me an hour to find, the post says so and shows what the symptom looked like, because reading a clean narrative teaches nothing about debugging.

Who this is for

You should be comfortable writing Java: classes, generics, collections, try-with-resources. You do not need to know anything about sockets, TCP, or Redis. Those are what the series teaches.

Never used Maven? Fine. You will use four commands and they are all below.

What you need

ToolWhyMinimum
JDKcompiles and runs everything21
Mavenbuild and test runner3.8
Gitone commit per stageany
redis-clithe real client, to test againstany
An IDEoptional but recommended

Nothing else. No Spring, no Netty, no Redis client library. That constraint is the point of the project. The pom.xml stays dependency-free apart from JUnit for tests.


Step 1, install a JDK

Java 21 or newer. I built this on 26, and the code uses several things from 21: virtual threads, SequencedCollection methods like addFirst, and pattern matching in switch. On 17 they will not compile.

macOS
brew install openjdk@21
sudo ln -sfn $(brew --prefix)/opt/openjdk@21/libexec/openjdk.jdk \
             /Library/Java/JavaVirtualMachines/openjdk-21.jdk
Linux (Debian, Ubuntu)
sudo apt update && sudo apt install openjdk-21-jdk
Windows
winget install EclipseAdoptium.Temurin.21.JDK
Any platform, using SDKMAN

Recommended if you juggle Java versions.

curl -s "https://get.sdkman.io" | bash
sdk install java 21.0.5-tem

Installers for every platform are also at Adoptium Temurin.

Run it

$ java -version
openjdk version "21.0.5" 2024-10-15

Notice that

The version must be 21 or higher. If it prints 17 or 11, the rest of this series will not compile, and the error will be confusing rather than obvious.


Step 2, install Maven

PlatformCommand
macOSbrew install maven
Linuxsudo apt install maven
Windowswinget install Apache.Maven
SDKMANsdk install maven

Or download from maven.apache.org.

Run it

$ mvn -v
Apache Maven 3.9.11
Java version: 21.0.5, vendor: Eclipse Adoptium

Notice that

mvn -v prints its own Java version on the second line. If that disagrees with what java -version said, Maven is compiling with a different JDK than you think, and you will get compile errors that make no sense. Fix it by setting JAVA_HOME:

export JAVA_HOME=$(/usr/libexec/java_home -v 21)      # macOS
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64   # Linux

Add that to ~/.zshrc or ~/.bashrc so it survives a new terminal.


Step 3, install redis-cli

People skip this, and it is the most important tool in the series. You are building a server. redis-cli is how you find out whether you actually speak the protocol or merely something that resembles it.

macOS
brew install redis
Linux
sudo apt install redis-tools      # client only, which is all you need
sudo apt install redis-server     # optional, useful for comparing behaviour
Windows

Redis has no official Windows build. Install WSL and follow the Linux instructions inside it. WSL also gives you the byte-inspection tools below, so it is the least painful route for the whole series.

wsl --install

Run it

$ redis-cli --version
redis-cli 8.2.0

Step 4, free up port 6379

If installing Redis started a server, stop it. It listens on 6379, the port you are about to claim.

brew services stop redis            # macOS
sudo systemctl stop redis-server    # Linux

Docker counts too. A container publishing 6379 holds the port just as effectively:

docker ps --format '{{.Names}}  {{.Ports}}'

Run it

$ lsof -nP -iTCP:6379 -sTCP:LISTEN

Notice that

Correct output is nothing at all. An empty response means the port is free.

If something prints, note the COMMAND column and stop that process before continuing.

Why this matters more than it looks, and the hour it cost me

My Stage 02 verification passed while a Homebrew Redis service was running. Real Redis had bound the specific addresses 127.0.0.1 and ::1. My server bound the wildcard 0.0.0.0. macOS permits that combination with SO_REUSEADDR, so there was no error, and nc localhost 6379 reached Redis rather than me.

My passing test was testing someone else’s server. On Linux it would have thrown BindException immediately.

The same thing happened later with a Docker container on port 6380. Check the port is free before you trust a result.

Keeping Redis installed is genuinely useful. Run it on a different port and compare behaviour command by command as you go:

redis-server --port 6380
redis-cli -p 6380 ping

Step 5, command-line tools

Four tools, used throughout to look at raw bytes.

ToolUsed for
nc (netcat)opening a raw TCP connection and typing at it
lsofchecking who owns a port
xxdhex dumps, for when you need to see the CRLF
printfsending exact bytes with no trailing newline

macOS and most Linux installs have all four. On Debian or Ubuntu:

sudo apt install netcat-openbsd lsof xxd

Run it

$ printf 'abc' | xxd
00000000: 6162 63    abc

Notice that

Three bytes, no newline. Use printf, not echo. echo appends a \n you did not ask for, and in a series about byte-exact protocols that extra byte changes the answer:

$ echo 'abc' | xxd
00000000: 6162 630a    abc.

That 0a is the difference between a passing and a failing test later.


Step 6, an IDE

Optional. Any editor works, but a Java-aware one saves real time.

IntelliJ IDEA Community is free. Open the project by pointing it at pom.xml. Its debugger is the fastest way to watch a parser walk a byte array, which you will want around Part 3.

VS Code with the Extension Pack for Java is lighter and perfectly fine.


Step 7, create the project

Pick a package name first. This series uses com.example.redis in every code block, and you can keep that or swap in your own. If you change it, change it everywhere, and note that the folders under src/main/java must match the package segments exactly. com.example.redis lives in src/main/java/com/example/redis.

mkdir build-redis-from-scratch
cd build-redis-from-scratch
mkdir -p src/main/java/com/example/redis
mkdir -p src/test/java/com/example/redis

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>redis</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.11.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Two notes on that file.

Use <maven.compiler.release>, not the source and target pair you will see in older tutorials. release also refuses to compile against APIs newer than the version you name, so you cannot accidentally depend on something your target JDK lacks.

JUnit is <scope>test</scope>, so it never ships in the runtime. That does not violate the no-frameworks rule. The rule is about not letting a framework do the Redis work for you.

Main.java

src/main/java/com/example/redis/Main.java:

package com.example.redis;

public class Main {
    public static void main(String[] args) {
        System.out.println("Redis server starting...");
    }
}

Write public static void main(String[] args) with the array. Java 25 lets you write void main() with no arguments, and I used it briefly. It compiles on 25 and up, and gives everyone else Main method not found.


Step 8, verify

$ mvn clean test
[INFO] BUILD SUCCESS

$ mvn -q clean compile
$ java -cp target/classes com.example.redis.Main
Redis server starting...

Those last two commands run every stage in this series.

Notice that

BUILD SUCCESS came from a project with zero tests. It proved your code compiles and the layout is valid. Nothing executed.

That distinction stops being academic the moment there is logic worth testing, and it is why tests do not appear in this series until Part 3. Before then there is nothing to test that is not just the JDK.


Try it yourself

  1. Run mvn clean test twice. The second is much faster, because Maven cached the plugin downloads.
  2. Break the pom.xml by deleting a closing tag and run it. Maven tells you the line. Put it back.
  3. Add a second class beside Main, compile, and confirm both .class files appear under target/classes. Then delete it. You now know where compiled output goes and how to clear it.

The rules for the whole series

No frameworks. No Spring, no Netty, no Jedis or Lettuce. The point is understanding what those abstractions hide.

No premature architecture. The project starts as one file and stays that way until a stage makes a single file genuinely painful. The first extra class arrives in Part 3, when there is something to test that cannot be tested through a socket.

Attempt before reading ahead. Every stage names the concepts to look up first. The stages are ordered so each limitation is felt before it is fixed. The fix means very little otherwise.

Checklist

✓ java -version                      → 21 or newer
✓ mvn -v                             → 3.8+, and the same JDK
✓ redis-cli --version
✓ lsof -nP -iTCP:6379 -sTCP:LISTEN   → prints nothing
✓ printf 'abc' | xxd                 → 6162 63, no 0a
✓ mvn clean test                     → BUILD SUCCESS
✓ the program runs and prints

Resources

Next

A TCP server that binds a port, accepts a connection, and teaches you why TCP has no messages, only bytes.

Part 1: TCP Before Redis →