Course Navigation
← Back to Course OverviewAll Lessons
 ✓ 
 Introduction and Database Fundamentals  ✓ 
 Building the Core Data Structure  ✓ 
 Concurrency and Thread Safety  ✓ 
 Append-Only Log (Write-Ahead Log)  ✓ 
 SSTables and LSM Trees  ✓ 
 Compaction and Optimization  7 
 TCP Server and Protocol Design  8 
 Client Library and Advanced Networking  9 
 Transactions and ACID Properties  10 
 Replication and High Availability  11 
 Monitoring, Metrics, and Observability  12 
 Performance Optimization and Tuning  13 
 Configuration and Deployment  14 
 Security and Production Hardening  15 
 Final Project and Beyond Current Lesson
  7 of 15 
  Progress 47% 
 TCP Server and Protocol Design
Learning Objectives
- • Master network protocol design principles
- • Implement RESP (Redis Protocol) from scratch
- • Build a production-ready TCP server in Go
- • Handle concurrent connections efficiently
- • Parse and validate network requests
Lesson 7.1: Network Protocol Design
Text-based vs Binary Protocols
When designing a database protocol, you must choose between text and binary approaches. Each has distinct advantages and trade-offs.
Text-based Protocol (Redis Protocol - RESP)
Example: SET user:1 alice
Text format (human-readable):
*3\r\n              # Array of 3 elements
$3\r\n              # First element: 3 bytes
SET\r\n             # Command
$6\r\n              # Second element: 6 bytes
user:1\r\n          # Key
$5\r\n              # Third element: 5 bytes
alice\r\n           # Value
Advantages:
- Human-readable for debugging
- Easy to test with telnet/nc
- Text editors can read/write
- Language-agnostic
Disadvantages:
- Larger wire size (more bytes)
- Parsing requires careful handling
- Line breaks matterBinary Protocol (Protocol Buffers, MessagePack)
Example: SET user:1 alice
Binary format (compact):
01              # Op: SET
06              # Key length: 6
75 73 65 72 3A 31  # "user:1"
05              # Value length: 5
61 6C 69 63 65  # "alice"
Advantages:
- Compact representation
- Faster parsing
- Type safety
- Smaller wire size
Disadvantages:
- Not human-readable
- Requires code generation
- Harder to debugDecision for our course: Use RESP (Redis Serialization Protocol)
- • Text-based (easier debugging)
- • Well-defined standard
- • Widely supported in clients
- • Simpler to implement correctly
RESP Protocol Specification
RESP has multiple data types for different use cases:
Simple String:    +OK\r\n
Error:            -ERR key not found\r\n
Integer:          :42\r\n
Bulk String:      $6\r\nhello!\r\n
Array:            *2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n
Null Bulk String: $-1\r\nSimple Strings
Format: +<string>\r\n
Examples:
+OK\r\n                    # Success response
+PONG\r\n                  # Ping responseErrors
Format: -<error message>\r\n
Examples:
-ERR unknown command\r\n
-ERR key not found\r\n
-WRONGTYPE Operation against a key holding wrong kind of value\r\nIntegers
Format: :<integer>\r\n
Examples:
:1000\r\n                  # Response to incr
:0\r\n                     # False/no
:1\r\n                     # True/yesBulk Strings
Format: $<length>\r\n<data>\r\n
Examples:
$6\r\nfoobar\r\n           # 6-byte string "foobar"
$-1\r\n                    # Null (key not found)
$0\r\n\r\n                 # Empty stringArrays
Format: *<count>\r\n<element1>...<elementN>
Examples:
*2\r\n
$4\r\nLLEN\r\n
$6\r\nmylist\r\n
*3\r\n
$3\r\nSET\r\n
$3\r\nkey\r\n
$5\r\nvalue\r\nComplete RESP Example
Client sends:
*3\r\n
$3\r\nSET\r\n
$6\r\nuser:1\r\n
$5\r\nalice\r\n
Server responds:
+OK\r\n
Client sends:
*2\r\n
$3\r\nGET\r\n
$6\r\nuser:1\r\n
Server responds:
$5\r\nalice\r\nRequest-Response Patterns
Synchronous Request-Response
Client connects:
  └─ TCP connection established
Client sends request:
  GET key\r\n
  
Server processes:
  └─ Reads key
  
Server sends response:
  $5\r\nvalue\r\n
Pattern repeats for next requestPipelining
Client sends multiple requests without waiting:
*2\r\n$3\r\nGET\r\n$2\r\nk1\r\n
*2\r\n$3\r\nGET\r\n$2\r\nk2\r\n
*2\r\n$3\r\nGET\r\n$2\r\nk3\r\n
Server processes and sends responses in order:
$2\r\nv1\r\n
$2\r\nv2\r\n
$2\r\nv3\r\n
Benefits:
- Reduces round-trip latency
- Increases throughput
- Better network utilizationLesson 7.2: TCP Server in Go
net Package Fundamentals
Go's `net` package provides low-level networking primitives for building servers and clients.
import "net"
// Listen creates TCP listener
listener, err := net.Listen("tcp", ":6379")
if err != nil {
    log.Fatal(err)
}
defer listener.Close()
// Accept connections
for {
    conn, err := listener.Accept()
    if err != nil {
        log.Printf("accept error: %v", err)
        continue
    }
    
    // Handle connection
    go handleConnection(conn)
}
func handleConnection(conn net.Conn) {
    defer conn.Close()
    
    // Read from connection
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    
    // Write to connection
    conn.Write([]byte("response"))
}Connection Handling Patterns
Goroutine-Per-Connection Model
type Server struct {
    listener net.Listener
    done     chan struct{}
}
func (s *Server) Start(addr string) error {
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    s.listener = listener
    
    go s.acceptLoop()
    return nil
}
func (s *Server) acceptLoop() {
    defer s.listener.Close()
    
    for {
        select {
        case <-s.done:
            return
        default:
        }
        
        conn, err := s.listener.Accept()
        if err != nil {
            select {
            case <-s.done:
                return
            default:
                log.Printf("accept error: %v", err)
            }
            continue
        }
        
        // Handle each connection in separate goroutine
        go s.handleConnection(conn)
    }
}
func (s *Server) handleConnection(conn net.Conn) {
    defer conn.Close()
    
    // Process client requests
    for {
        // Read request
        // Process
        // Write response
    }
}
func (s *Server) Stop() {
    close(s.done)
}Goroutine-Per-Connection Trade-offs
Advantages:
- • Simple to implement
- • Good for many concurrent connections
- • Go's scheduler handles goroutines efficiently
Disadvantages:
- • Memory per goroutine (~2MB)
- • Can have 100K+ goroutines
- • Context switching overhead
Typical limits: 10K concurrent connections per machine. High-performance servers use event loops (epoll, kqueue).
Connection State Management
type ClientState struct {
    conn        net.Conn
    reader      *bufio.Reader
    writer      *bufio.Writer
    lastActive  time.Time
    authenticated bool
    db          int  // Selected database number
}
func (cs *ClientState) Update() {
    cs.lastActive = time.Now()
}
func (cs *ClientState) Idle() time.Duration {
    return time.Since(cs.lastActive)
}
// Per-connection context
type ConnectionContext struct {
    state       *ClientState
    txn         *Transaction  // If in transaction
    subscription *Subscription  // If subscribed
}Connection Pooling
// Server-side pooling (limit concurrent connections)
type ConnPool struct {
    maxConns  int
    current   int
    mu        sync.Mutex
    waitQueue chan struct{}
}
func (cp *ConnPool) Acquire() error {
    cp.mu.Lock()
    if cp.current >= cp.maxConns {
        cp.mu.Unlock()
        // Wait for connection to be available
        <-cp.waitQueue
        cp.mu.Lock()
    }
    cp.current++
    cp.mu.Unlock()
    return nil
}
func (cp *ConnPool) Release() {
    cp.mu.Lock()
    cp.current--
    cp.mu.Unlock()
    
    // Signal waiting connection
    select {
    case cp.waitQueue <- struct{}{}:
    default:
    }
}Lesson 7.3: Request Parsing
Efficient Buffer Management
import "bufio"
type RESPParser struct {
    reader *bufio.Reader
}
func NewRESPParser(conn net.Conn) *RESPParser {
    return &RESPParser{
        reader: bufio.NewReaderSize(conn, 64*1024),  // 64KB buffer
    }
}
// Read line (until \r\n)
func (p *RESPParser) readLine() ([]byte, error) {
    line, err := p.reader.ReadBytes('\n')
    if err != nil {
        return nil, err
    }
    
    // Remove \r\n
    if len(line) > 0 && line[len(line)-1] == '\n' {
        line = line[:len(line)-1]
    }
    if len(line) > 0 && line[len(line)-1] == '\r' {
        line = line[:len(line)-1]
    }
    
    return line, nil
}
// Peek into buffer without consuming
func (p *RESPParser) peek() (byte, error) {
    b, err := p.reader.Peek(1)
    if len(b) > 0 {
        return b[0], nil
    }
    return 0, err
}
// Read N bytes exactly
func (p *RESPParser) readN(n int) ([]byte, error) {
    buf := make([]byte, n)
    _, err := io.ReadFull(p.reader, buf)
    return buf, err
}Streaming Large Values
// For very large values, stream instead of buffering all
func (p *RESPParser) ParseBulkString(maxSize int64) (io.Reader, error) {
    // Parse length
    line, _ := p.readLine()
    size := parseBulkSize(line)
    
    if size > maxSize {
        return nil, ErrValueTooLarge
    }
    
    // Return limited reader
    return io.LimitReader(p.reader, size), nil
}Protocol State Machines
type ParserState int
const (
    StateWaitingForCommand ParserState = iota
    StateWaitingForArgs
    StateWaitingForBulk
)
type StatefulParser struct {
    state    ParserState
    command  string
    args     [][]byte
    reader   *bufio.Reader
}
func (sp *StatefulParser) Parse() (*Command, error) {
    switch sp.state {
    case StateWaitingForCommand:
        return sp.parseCommand()
    case StateWaitingForArgs:
        return sp.parseArgs()
    case StateWaitingForBulk:
        return sp.parseBulk()
    }
    return nil, ErrInvalidState
}
func (sp *StatefulParser) parseCommand() (*Command, error) {
    // Read array marker
    marker, _ := sp.peek()
    if marker != '*' {
        return nil, ErrInvalidFormat
    }
    
    // Parse array length
    line, _ := sp.readLine()
    count := parseArrayCount(line)
    
    sp.state = StateWaitingForArgs
    return nil, ErrNeedMoreData
}Complete Server Implementation
package server
import (
    "bufio"
    "context"
    "fmt"
    "io"
    "log"
    "net"
    "strconv"
    "sync"
    "time"
)
// Server accepts client connections and processes commands
type Server struct {
    addr        string
    listener    net.Listener
    done        chan struct{}
    wg          sync.WaitGroup
    db          Store
    maxConns    int
    connCount   int
    connMu      sync.Mutex
}
// NewServer creates a new server
func NewServer(addr string, db Store) *Server {
    return &Server{
        addr:     addr,
        done:     make(chan struct{}),
        db:       db,
        maxConns: 10000,
    }
}
// Start begins listening for connections
func (s *Server) Start() error {
    listener, err := net.Listen("tcp", s.addr)
    if err != nil {
        return err
    }
    s.listener = listener
    
    log.Printf("Server listening on %s", s.addr)
    
    s.wg.Add(1)
    go s.acceptLoop()
    
    return nil
}
// acceptLoop accepts incoming connections
func (s *Server) acceptLoop() {
    defer s.wg.Done()
    defer s.listener.Close()
    
    for {
        select {
        case <-s.done:
            return
        default:
        }
        
        conn, err := s.listener.Accept()
        if err != nil {
            select {
            case <-s.done:
                return
            default:
                log.Printf("accept error: %v", err)
            }
            continue
        }
        
        // Check connection limit
        s.connMu.Lock()
        if s.connCount >= s.maxConns {
            s.connMu.Unlock()
            conn.Close()
            continue
        }
        s.connCount++
        s.connMu.Unlock()
        
        s.wg.Add(1)
        go s.handleClient(conn)
    }
}
// handleClient processes commands from a client
func (s *Server) handleClient(conn net.Conn) {
    defer s.wg.Done()
    defer conn.Close()
    defer func() {
        s.connMu.Lock()
        s.connCount--
        s.connMu.Unlock()
    }()
    
    // Set connection timeouts
    conn.SetDeadline(time.Now().Add(30 * time.Second))
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))
    conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
    
    parser := NewRESPParser(bufio.NewReader(conn))
    writer := bufio.NewWriter(conn)
    defer writer.Flush()
    
    ctx := context.Background()
    
    for {
        // Parse next command
        cmd, err := parser.ParseCommand()
        if err == io.EOF {
            return
        }
        if err != nil {
            s.sendError(writer, fmt.Sprintf("ERR %v", err))
            continue
        }
        
        // Reset timeout on each successful command
        conn.SetDeadline(time.Now().Add(30 * time.Second))
        conn.SetReadDeadline(time.Now().Add(30 * time.Second))
        conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
        
        // Execute command
        s.executeCommand(ctx, cmd, writer)
    }
}
// executeCommand processes a single command
func (s *Server) executeCommand(ctx context.Context, cmd *Command, w *bufio.Writer) {
    switch cmd.Name {
    case "GET":
        s.handleGet(ctx, cmd, w)
    case "SET":
        s.handleSet(ctx, cmd, w)
    case "DEL":
        s.handleDel(ctx, cmd, w)
    case "PING":
        s.sendSimpleString(w, "PONG")
    case "ECHO":
        if len(cmd.Args) < 1 {
            s.sendError(w, "ERR wrong number of arguments for 'echo' command")
            return
        }
        s.sendBulkString(w, cmd.Args[0])
    case "INFO":
        s.handleInfo(w)
    default:
        s.sendError(w, fmt.Sprintf("ERR unknown command '%s'", cmd.Name))
    }
    w.Flush()
}
// handleGet executes GET command
func (s *Server) handleGet(ctx context.Context, cmd *Command, w *bufio.Writer) {
    if len(cmd.Args) != 1 {
        s.sendError(w, "ERR wrong number of arguments for 'get' command")
        return
    }
    
    value, err := s.db.Get(ctx, cmd.Args[0])
    if err != nil {
        s.sendNullBulkString(w)
        return
    }
    
    s.sendBulkString(w, value)
}
// handleSet executes SET command
func (s *Server) handleSet(ctx context.Context, cmd *Command, w *bufio.Writer) {
    if len(cmd.Args) != 2 {
        s.sendError(w, "ERR wrong number of arguments for 'set' command")
        return
    }
    
    err := s.db.Put(ctx, cmd.Args[0], cmd.Args[1])
    if err != nil {
        s.sendError(w, fmt.Sprintf("ERR %v", err))
        return
    }
    
    s.sendSimpleString(w, "OK")
}
// handleDel executes DEL command
func (s *Server) handleDel(ctx context.Context, cmd *Command, w *bufio.Writer) {
    if len(cmd.Args) == 0 {
        s.sendError(w, "ERR wrong number of arguments for 'del' command")
        return
    }
    
    deleted := 0
    for _, key := range cmd.Args {
        err := s.db.Delete(ctx, key)
        if err == nil {
            deleted++
        }
    }
    
    s.sendInteger(w, int64(deleted))
}
// handleInfo returns server info
func (s *Server) handleInfo(w *bufio.Writer) {
    s.connMu.Lock()
    connCount := s.connCount
    s.connMu.Unlock()
    
    info := fmt.Sprintf("# Server\r\nconnected_clients:%d\r\n", connCount)
    s.sendBulkString(w, []byte(info))
}
// Response writers
func (s *Server) sendSimpleString(w *bufio.Writer, msg string) {
    fmt.Fprintf(w, "+%s\r\n", msg)
}
func (s *Server) sendError(w *bufio.Writer, msg string) {
    fmt.Fprintf(w, "-%s\r\n", msg)
}
func (s *Server) sendInteger(w *bufio.Writer, n int64) {
    fmt.Fprintf(w, ":%d\r\n", n)
}
func (s *Server) sendBulkString(w *bufio.Writer, data []byte) {
    fmt.Fprintf(w, "$%d\r\n", len(data))
    w.Write(data)
    fmt.Fprintf(w, "\r\n")
}
func (s *Server) sendNullBulkString(w *bufio.Writer) {
    fmt.Fprintf(w, "$-1\r\n")
}
// Stop gracefully shuts down the server
func (s *Server) Stop() {
    close(s.done)
    s.wg.Wait()
}
// Command represents a parsed client command
type Command struct {
    Name string
    Args [][]byte
}
// Store interface for persistence
type Store interface {
    Get(ctx context.Context, key []byte) ([]byte, error)
    Put(ctx context.Context, key, value []byte) error
    Delete(ctx context.Context, key []byte) error
}
// RESPParser parses Redis Serialization Protocol
type RESPParser struct {
    reader *bufio.Reader
}
// NewRESPParser creates a new parser
func NewRESPParser(r *bufio.Reader) *RESPParser {
    return &RESPParser{reader: r}
}
// ParseCommand parses next RESP command
func (p *RESPParser) ParseCommand() (*Command, error) {
    // Read array marker
    marker, err := p.reader.ReadByte()
    if err != nil {
        return nil, err
    }
    
    if marker != '*' {
        return nil, fmt.Errorf("expected '*', got %c", marker)
    }
    
    // Read array length
    line, err := p.readLine()
    if err != nil {
        return nil, err
    }
    
    count, err := strconv.Atoi(string(line))
    if err != nil {
        return nil, fmt.Errorf("invalid array length: %v", err)
    }
    
    if count == 0 {
        return nil, fmt.Errorf("empty array")
    }
    
    // Read first element (command name)
    cmdName, err := p.parseBulkString()
    if err != nil {
        return nil, err
    }
    
    // Read remaining arguments
    args := make([][]byte, count-1)
    for i := 0; i < count-1; i++ {
        arg, err := p.parseBulkString()
        if err != nil {
            return nil, err
        }
        args[i] = arg
    }
    
    return &Command{
        Name: string(cmdName),
        Args: args,
    }, nil
}
// parseBulkString parses a RESP bulk string
func (p *RESPParser) parseBulkString() ([]byte, error) {
    marker, err := p.reader.ReadByte()
    if err != nil {
        return nil, err
    }
    
    if marker != '$' {
        return nil, fmt.Errorf("expected '$', got %c", marker)
    }
    
    line, err := p.readLine()
    if err != nil {
        return nil, err
    }
    
    length, err := strconv.Atoi(string(line))
    if err != nil {
        return nil, fmt.Errorf("invalid length: %v", err)
    }
    
    if length == -1 {
        return nil, nil  // Null bulk string
    }
    
    // Read data
    data := make([]byte, length)
    _, err = io.ReadFull(p.reader, data)
    if err != nil {
        return nil, err
    }
    
    // Read trailing \r\n
    _, err = p.readLine()
    if err != nil {
        return nil, err
    }
    
    return data, nil
}
// readLine reads until \r\n
func (p *RESPParser) readLine() ([]byte, error) {
    line, err := p.reader.ReadBytes('\n')
    if err != nil {
        return nil, err
    }
    
    // Remove \r\n
    if len(line) > 0 && line[len(line)-1] == '\n' {
        line = line[:len(line)-1]
    }
    if len(line) > 0 && line[len(line)-1] == '\r' {
        line = line[:len(line)-1]
    }
    
    return line, nil
}Lab 7.1: Implement Redis Protocol Server
Objective
Build a complete RESP-compatible TCP server with connection handling, command parsing, and response generation.
Requirements
- • TCP Server: Listen on configurable address, accept multiple concurrent connections
- • RESP Protocol: Parse all RESP data types, handle errors gracefully
- • Command Handlers: GET, SET, DEL, PING, ECHO, INFO
- • Error Handling: Invalid commands, wrong argument counts, malformed RESP
- • Testing: Test with redis-cli, concurrent client tests
- • Benchmarks: Throughput (commands/sec), connection setup overhead
Starter Code
package server
import (
    "bufio"
    "context"
    "log"
    "net"
    "sync"
    "time"
)
// Server accepts and handles client connections
type Server struct {
    addr    string
    // TODO: Add fields for listener, storage, lifecycle
}
// NewServer creates a new server
// TODO: Implement
func NewServer(addr string, db Store) *Server {
    return nil
}
// Start begins listening
// TODO: Implement
func (s *Server) Start() error {
    return nil
}
// acceptLoop accepts incoming connections
// TODO: Implement goroutine-per-connection model
func (s *Server) acceptLoop() {
}
// handleClient processes commands from one client
// TODO: Implement command loop with parser
func (s *Server) handleClient(conn net.Conn) {
}
// executeCommand processes a single command
// TODO: Implement dispatch to handlers
func (s *Server) executeCommand(ctx context.Context, cmd *Command, w io.Writer) {
}
// Command handlers
// TODO: Implement handleGet, handleSet, handleDel
// Response writers
// TODO: Implement sendSimpleString, sendError, sendBulkString, etc
// Stop gracefully shuts down
// TODO: Implement
func (s *Server) Stop() {
}
// RESPParser parses RESP format
type RESPParser struct {
    // TODO: Add fields
}
// NewRESPParser creates parser
// TODO: Implement
func NewRESPParser(r *bufio.Reader) *RESPParser {
    return nil
}
// ParseCommand parses next command
// TODO: Implement complete RESP parsing
func (p *RESPParser) ParseCommand() (*Command, error) {
    return nil, nil
}
// Command represents parsed command
type Command struct {
    Name string
    Args [][]byte
}
// Store interface
type Store interface {
    Get(ctx context.Context, key []byte) ([]byte, error)
    Put(ctx context.Context, key, value []byte) error
    Delete(ctx context.Context, key []byte) error
}Test Template
func TestServerBasic(t *testing.T) {
    // Create mock store
    store := &MockStore{data: make(map[string][]byte)}
    
    // Start server
    server := NewServer("localhost:0", store)
    if err := server.Start(); err != nil {
        t.Fatal(err)
    }
    defer server.Stop()
    
    // Connect client
    conn, _ := net.Dial("tcp", server.Addr())
    defer conn.Close()
    
    // Send SET command
    fmt.Fprintf(conn, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n")
    
    // Read response
    reader := bufio.NewReader(conn)
    resp, _ := reader.ReadString('\n')
    
    if resp != "+OK\r\n" {
        t.Errorf("expected +OK, got %s", resp)
    }
}
func BenchmarkServerThroughput(b *testing.B) {
    store := &MockStore{data: make(map[string][]byte)}
    server := NewServer("localhost:0", store)
    server.Start()
    defer server.Stop()
    
    conn, _ := net.Dial("tcp", server.Addr())
    defer conn.Close()
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        fmt.Fprintf(conn, "*2\r\n$4\r\nPING\r\n")
        // Read response
        bufio.NewReader(conn).ReadString('\n')
    }
}Acceptance Criteria
- ✅ All tests pass
- ✅ Handles concurrent clients
- ✅ Parses RESP correctly
- ✅ redis-cli compatible
- ✅ Graceful shutdown
- ✅ 100K+ commands/sec throughput
- ✅ > 80% code coverage
Summary: Week 7 Complete
By completing Week 7, you've learned:
1. Protocol Design
- • Text-based vs binary protocols
- • RESP (Redis Protocol) specification
- • Request-response patterns
- • Pipelining support
2. TCP Server Fundamentals
- • Go's `net` package
- • Goroutine-per-connection model
- • Connection lifecycle management
- • Timeouts and error handling
3. Request Parsing
- • Efficient buffer management
- • RESP parsing state machine
- • Streaming large values
- • Protocol validation
4. Complete Server Implementation
- • Full working TCP server
- • RESP protocol parser
- • Command execution framework
- • Response writers
- • Connection pooling basics
Metrics Achieved:
- ✅ Handles 10,000+ concurrent connections
- ✅ Sub-millisecond command parsing
- ✅ Graceful shutdown with connection draining
- ✅ Connection timeouts to prevent hangs
Ready for Week 8?
Next week we'll build a production-ready client library with connection pooling, retry logic, circuit breakers, and advanced networking features.
Continue to Week 8: Client Library →