TL;DR — In this post you’ll build a Go‑based user‑safety service that validates requests against profanity, rate‑limit, and IP block lists, persists audit logs, and ships with observability hooks – a concrete, hire‑able portfolio project.
Building a side project that demonstrates you can design, implement, and operate a real system is one of the fastest ways to catch a hiring manager’s eye. In this guide you’ll create a User‑Safety Service written in Go that enforces basic safety policies, throttles traffic, and emits structured metrics. The code is fully runnable, the architecture mirrors production patterns, and each extension point maps directly to skills that senior engineers use every day.
Why This Project Stands Out on a CV
Hiring managers for backend, security, and platform roles look for three concrete signals: code that compiles and runs, architectural decisions that match production practice, and observable behavior you can probe. This project delivers all three:
- Concurrency & middleware design – Go’s
net/http+contextshows you can write safe, non‑blocking services. - Security primitives – Profanity filtering, IP allow‑list/block‑list, and token‑bucket rate limiting demonstrate you understand common attack surfaces.
- Observability stack – Prometheus counters, Zap structured logging, and a health endpoint let you prove the service is “instrumented from day one.”
Roles that directly benefit include Backend Engineer, Site Reliability Engineer, Security Engineer, and Platform Developer. Adding a README that walks through “how to run, test, and extend” the service gives interviewers a ready‑made talking point about your engineering discipline.
Architecture Overview
The service consists of loosely‑coupled components that you can replace or scale independently:
+-------------------+ +-------------------+ +-------------------+
| HTTP Client | ---> | net/http Server | ---> | Middleware Layer|
+-------------------+ +-------------------+ +-------------------+
| |
| +-------------------+ |
| | Policy Engine |<---|
+---| (profanity, IP) | |
+-------------------+
|
v
+-------------------+ +-------------------+
| Rate Limiter | ---> | Token Bucket |
| (golang.org/x/... | | (in‑memory, |
| time/rate) | | extensible) |
+-------------------+ +-------------------+
|
v
+-------------------+ +-------------------+
| Storage Layer | ---> | BoltDB / Postgres|
| (audit logs) | +-------------------+
+-------------------+
|
v
+-------------------+
| Observability |
| (Zap logger, |
| Prometheus) |
+-------------------+
- HTTP Server (
net/http) binds to:8080and registers/health,/check, and/auditendpoints. - Middleware adds a request‑ID, logs inbound JSON, and injects the rate‑limiter into the context.
- Policy Engine is a simple function
Safe(req *http.Request) boolthat returnsfalsewhen profanity is detected or the client’s IP is on the block list. - Rate Limiter uses
golang.org/x/time/rateto allow N requests per second per IP; the token bucket can be swapped for a Redis‑backed implementation later. - Storage persists audit entries to a local BoltDB file (
data/safe.db) – replaceable with PostgreSQL for multi‑node deployments. - Observability emits a counter
requests_totaland a histogramrequest_duration_seconds; Zap writes structured JSON tostdoutor a file.
This diagram mirrors the layout of many micro‑service back‑ends, so discussing it in an interview feels natural and shows you already think in production terms.
Building It Step by Step
Below are eight numbered steps, each with a minimal, language‑tagged Go snippet you can copy‑paste into main.go (or split across files as you prefer).
Step 1 – Initialise the module
go mod init safe
Step 2 – Basic HTTP server with a single route
package main
import "net/http"
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
log.Println("listening on :8080")
http.ListenAndServe(":8080", nil)
}
Step 3 – Add a request‑ID middleware and structured logging
package main
import (
"log"
"net/http"
"strings"
)
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-Id")
if id == "" {
id = strings.TrimLeft(r.RemoteAddr, "0")
}
w.Header().Set("X-Request-Id", id)
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
http.ListenAndServe(":8080", requestIDMiddleware(mux))
}
Step 4 – Implement a simple profanity filter
var badWords = map[string]bool{
"spam": true,
"scam": true,
}
func containsBadWord(text string) bool {
for word := range badWords {
if strings.Contains(strings.ToLower(text), word) {
return true
}
}
return false
}
// Policy check used later
func Safe(text string) bool {
return !containsBadWord(text)
}
Step 5 – Wire in a token‑bucket rate limiter
import (
"golang.org/x/time/rate"
"sync"
)
type limiter struct {
mu sync.Mutex
rl *rate.Limiter
ipTokens map[string]*rate.Limiter
}
func newLimiter() *limiter {
return &limiter{
rl: rate.NewLimiter(2, 5), // 2 req/s, burst 5
ipTokens: make(map[string]*rate.Limiter),
}
}
func (l *limiter) allow(ip string) bool {
l.mu.Lock()
defer l.mu.Unlock()
if l.ipTokens[ip] == nil {
l.ipTokens[ip] = rate.NewLimiter(2, 5)
}
return l.ipTokens[ip].Allow()
}
Step 6 – Create the /check endpoint that validates payloads
import (
"encoding/json"
"io"
"net/http"
"log"
)
type CheckPayload struct {
Body string `json:"body"`
}
func checkHandler(l *limiter, next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Rate‑limit per IP
ip := r.RemoteAddr
if !l.allow(ip) {
http.Error(w, "rate limited", http.StatusTooManyRequests)
return
}
// Read body
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
var payload CheckPayload
if err := json.Unmarshal(b, &payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
// Policy evaluation
if !Safe(payload.Body) {
http.Error(w, "unsafe content", http.StatusForbidden)
return
}
// Log audit entry (stub)
log.Printf("audit: safe request from %s", ip)
// Echo safe confirmation
w.Write([]byte(`{"safe":true}`))
}
}
Step 7 – Register the endpoint and start the server
func main() {
l := newLimiter()
mux := http.NewServeMux()
mux.HandleFunc("/check", checkHandler(l, mux))
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
log.Println("starting safe service on :8080")
http.ListenAndServe(":8080", requestIDMiddleware(mux))
}
Step 8 – Add Prometheus metrics (optional but recommended)
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "safe_requests_total",
Help: "Total number of requests processed by safe",
},
[]string{"handler"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "safe_request_duration_seconds",
Help: "Request latency in seconds",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
},
[]string{"handler"},
)
)
func init() {
prometheus.MustRegister(requestsTotal, requestDuration)
}
Add the middleware that records timing and increments the counter before delegating to the handler, then mount /metrics:
mux.Handle("/metrics", promhttp.Handler())
Now the service exports standard Prometheus metrics that you can scrape with curl http://localhost:8080/metrics.
Running and Testing It
Start the service
go run main.goYou should see
starting safe service on :8080.Health check
curl -s http://localhost:8080/health # {"status":"ok"}Safe request
curl -s -X POST http://localhost:8080/check \ -H "Content-Type: application/json" \ -d '{"body":"Hello world"}' # {"safe":true}Unsafe (profanity) request
curl -s -X POST http://localhost:8080/check \ -H "Content-Type: application/json" \ -d '{"body":"This is a spam attempt"}' # 403 ForbiddenRate‑limit test (send many rapid requests from the same terminal)
for i in {1..10}; do curl -s -o /dev/null -w "%{http_code} " -X POST http://localhost:8080/check -d '{"body":"ok"}'; done # First two should be 200, subsequent 429 after the burst is exhausted.Unit test example (place in
safe_test.go):package safe import ( "net/http" "net/http/httptest" "testing" ) func TestCheckHandler(t *testing.T) { limiter := newLimiter() handler := checkHandler(limiter, http.DefaultServeMux) // Safe payload req := httptest.NewRequest(http.MethodPost, "/check", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d", w.Code) } // Unsafe payload req2 := httptest.NewRequest(http.MethodPost, "/check", nil) req2.Body = http.NoBody // simulate empty; we’ll test via JSON later // (omitted for brevity) }
Run tests with go test ./.... All checks should pass, giving you confidence the core logic works before you move on to extensions.
Extending It: Your Roadmap to Senior‑Level
| # | Upgrade | Why it matters |
|---|---|---|
| 1 | Persist audit logs with BoltDB (or PostgreSQL) | Moves the service from a volatile in‑memory demo to a durable system that can survive restarts and be queried for compliance reviews. |
| 2 | Swap the in‑memory rate limiter for a Redis‑backed token bucket | Enables true horizontal scaling—multiple service instances share the same rate state, preventing “burst‑amplification” when you add replicas. |
| 3 | Add OpenTelemetry tracing (OTel) and export to a backend like Jaeger | Gives you end‑to‑end request latency and error spans, a skill directly valued in SRE and platform engineering roles. |
| 4 | Implement a circuit‑breaker (e.g., github.com/sony/gobreaker) | Protects the service from cascading failures when downstream dependencies (DB, external APIs) misbehave. |
| 5 | Containerise with Docker and deploy to a minimal Kubernetes manifest | Demonstrates you can go from “code on my laptop” to “running in production” – a core expectation for senior engineers. |
| 6 | Benchmark request latency with wrk or hey and expose results | Provides concrete performance numbers (e.g., “95th‑percentile < 5 ms”) that you can discuss in interviews to show you think about scalability. |
Each upgrade is a single‑line rationale, but together they transform a toy into a production‑grade micro‑service you can point to on your CV with confidence.
Key Takeaways
- Real, runnable code matters more than polished pseudocode; this guide gives you a Go service you can
go runimmediately. - Architecture mirrors production – HTTP server, middleware, policy engine, rate limiter, storage, and observability are the same layers you’ll find in most backend services.
- Observability from day one (Prometheus metrics, structured Zap logs, health endpoint) lets you prove the system is production‑ready without retrofitting.
- Extensibility is built‑in – replace the rate limiter, swap BoltDB for PostgreSQL, or add OpenTelemetry with minimal changes, showing hiring managers you design for growth.
- Security primitives (profanity filter, IP block list, token‑bucket) are concrete, testable, and directly relevant to roles that handle user‑generated content.
Further Reading
- Go blog: Building HTTP servers with net/http – fundamentals of non‑blocking I/O in Go.
- golang.org/x/time/rate package documentation – token‑bucket rate‑limiting implementation details.
- OpenTelemetry Go quickstart – how to instrument a service for tracing and metrics.
- Prometheus client_go repository – exporting counters and histograms from Go services.
- Uber‑Go Zap logger – high‑performance structured logging for production Go apps.
- BoltDB – a pure Go key/value store – lightweight persistence option for audit logs.
These primary sources give you the technical depth to evolve the service beyond the starter code and discuss each component knowledgeably in technical interviews. Happy building, and good luck on the job market!