-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
250: Interview Practice: System Design Basics for Go Backend Roles
When you hit the system design portion of a Go interview, the interviewer isn't just looking for your ability to draw boxes and arrows on a whiteboard. They want to see if you understand how Go's concurrency model interacts with the physical limits of a distributed system. A common mistake I see is candidates treating a Go service as a magic black box that scales infinitely just because goroutines are cheap. They forget that while CPU and memory are manageable, the network and the database are almost always the bottleneck.
The 'Global Map and Mutex' Trap
Imagine we're building a real-time API hit counter. You need to track how many requests each endpoint is receiving per minute. The naive approach—the one that works perfectly on your local machine but fails the interview—is to create a global map protected by a sync.Mutex. You'd probably write something where every single incoming request locks the mutex, increments the count for the specific URL, and then unlocks it.
type Counter struct {
mu sync.Mutex
counts map[string]int
}
func (c *Counter) Inc(url string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counts[url]++
}
On the surface, it looks clean. But in a high-traffic backend role, this is a red flag. You've created a massive point of contention. As your request volume grows, your goroutines will spend more time waiting for that single lock than actually doing work. Even worse, this approach only works if you have one single server. The second you scale to two or three pods in a Kubernetes cluster, your data is fragmented. Pod A has no idea what Pod B is counting, and your "system" is now just a collection of isolated islands.
Batching Local State Before the Network Hop
To fix this, we have to move the state to a distributed store like Redis, but we can't just replace the Mutex with a network call. If you call INCR in Redis for every single request, you're trading a memory lock for network latency. Your throughput will plummet because you're now bound by the round-trip time (RTT) of your network.
The professional way to handle this in Go is to embrace the "fan-in" pattern. Instead of updating the global state immediately, you treat the request handler as a producer that sends an event into a buffered channel. Then, you have a separate worker goroutine that consumes those events, aggregates them in a local map for a short window (say, one second), and performs a single batch update to Redis.
type Event struct {
URL string
}
func (s *Server) startAggregator(ctx context.Context, events <-chan Event) {
ticker := time.NewTicker(1 * time.Second)
localCounts := make(map[string]int)
for {
select {
case e := <-events:
localCounts[e.URL]++
case <-ticker.C:
if len(localCounts) == 0 {
continue
}
s.flushToRedis(localCounts)
localCounts = make(map[string]int) // Reset for next window
case <-ctx.Done():
return
}
}
}
I prefer this approach because it decouples the request latency from the persistence latency. The user gets a response immediately, and the system handles the heavy lifting asynchronously. We've shifted the bottleneck from a locking mechanism to a throughput-oriented pipeline.
The Trade-off: Consistency vs. Throughput
Now, if you propose this in an interview, a sharp interviewer will immediately ask: "What happens if the pod crashes before the ticker fires?" This is where you show your seniority. You admit that you've traded strong consistency for high availability and throughput.
In the naive Mutex version, if the process survives, the data is "accurate" (for that one instance). In the batching version, you might lose up to one second of data if a pod is killed. In most analytics scenarios, losing 0.1% of hits is an acceptable trade-off for a 10x increase in request capacity. If the data must be 100% accurate—like in a financial ledger—you can't use this pattern; you'd need a write-ahead log (WAL) or a distributed queue like Kafka. Knowing when to use which pattern is exactly what "System Design" is actually about.
📋 Practical Task
Implement a Buffered Event Aggregator with Graceful Shutdown
Your task is to build a production-ready version of the event aggregator discussed in the lesson. You need to ensure that no data is lost when the service shuts down.
Requirements:
- Create an
Aggregatorstruct that accepts events via a channel. - Implement a
Flush()method that simulates writing the current local map to a database (usefmt.Printfto show the counts being "saved"). - Implement a
Stop()method that closes the event channel and ensures all remaining buffered events are flushed to the database before the function returns. - Use a
time.Tickerto trigger automatic flushes every 500 milliseconds.
Constraint: Your Stop() method must not simply exit; it must ensure the final state of the local map is processed. Think about how you can signal the worker goroutine to perform one last flush after the channel is closed.
There are no comments for now.