A high-performance, production-grade User Management Microservice built with Go 1.24+.
This project demonstrates Advanced Backend Engineering concepts including Clean Architecture, Distributed Caching, Event-Driven Messaging, and Resiliency Patterns.
| Component | Technology | Role |
|---|---|---|
| Language | Go (Golang) | Core Logic (Standard Lib + optimized drivers) |
| Database | PostgreSQL | Primary Data Store (Source of Truth) |
| Driver | pgx | High-performance PostgreSQL driver |
| Cache | Redis | Distributed Cache (Cache-Aside Pattern) |
| Messaging | RabbitMQ | Asynchronous Event Bus (AMQP) |
| Resiliency | Singleflight | Cache Stampede Protection |
| Deployment | Docker Compose | Container Orchestration |
Separation of concerns is mostly strict:
- Transport: HTTP Handlers & Middleware.
- Service: Business Logic (Hashing, Caching, Event Publishing).
- Repository: Raw SQL Queries (
database/sql). - Domain: Pure Go structs.
- Redis Cache-Aside: Reads check Redis first (Sub-millisecond latency). Falls back to DB only on miss.
- Singleflight: Protects the database from "Cache Stampedes". If 10,000 requests hit a cold cache simultaneously, only ONE DB query is fired. The result is shared with all 10,000 waiters.
- Critical Path: DB Creation is synchronous (User gets 201 Created immediately).
- Background Path: RabbitMQ Event is published. A separate Worker Process consumes this event to handle slow tasks (e.g., Welcome Emails, Analytics) without blocking the API.
- The worker intercepts
SIGTERM/SIGINT. - It stops accepting new tasks but waits for in-flight tasks to finish processing before exiting.
- Prevents data corruption and dropped jobs during deployments.
logs of testing using bombardier (i know this is cache read and not db writes, its bcz i wanted to know the best performance of the system. if we were to try diff user id's each time, it'd settile around 1k-2k RPS max bcz then we are involving bcrypt,marshalling,DB R/W that takes some time)
PS C:\Users\vaswa\Desktop\placemet_projs\go_adv_proj_1> bombardier -c 100 -d 10s -p r -l http://localhost:8080/users/89af8930-24b3-4d17-8906-254894b7e591
Statistics Avg Stdev Max
Reqs/sec 11842.59 3527.45 18250.99
Latency 8.45ms 13.46ms 822.55ms
Latency Distribution
50% 7.29ms
75% 9.85ms
90% 12.94ms
95% 15.54ms
99% 23.03ms
HTTP codes:
1xx - 0, 2xx - 118210, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 3.91MB/s
- Docker & Docker Compose
# Starts API, Worker, Postgres, Redis, RabbitMQ
docker-compose up --buildThe API is now running on localhost:8080.
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"email": "engineer@example.com", "password": "securePass123"}'Response: 201 Created with User ID.
#1st call (db hit -> cache write will happen)
curl http://localhost:8080/users/{USER_ID}
#2nd call (redis hit - scary fast)
curl http://localhost:8080/users/{USER_ID}curl -X DELETE http://localhost:8080/users/{USER_ID}Effect: Removes user from both PostgreSQL and Redis.
- Navigate to
cmd/worker/main.go. - Uncomment the
time.Sleep(10 * time.Second)line to simulate a slow task. - Trigger a user creation.
- Immediately run
docker stop <worker_container_id>. - Observation: The container will NOT stop immediately. It will wait for the 10s task to finish, then log "Stopped Gracefully".
.
├── cmd/
│ ├── api/ # REST API Main entrypoint
│ └── worker/ # Background Worker Main entrypoint
├── internal/
│ ├── config/ # Env loading
│ ├── domain/ # Struct Definitions
│ ├── infrastructure/ # Redis & RabbitMQ Clients
│ ├── repository/ # SQL Logic
│ ├── service/ # Business Logic (The Glue)
│ └── transport/ # HTTP Handlers
├── migrations/ # SQL Migration files (Embedded)
├── docker-compose.yml # Infrastructure definition
└── Dockerfile # Multi-stage build
import "adv-bknd/migrations"var FS embed.FSimport "adv-bknd/cmd/api"import "adv-bknd/cmd/worker"import "adv-bknd/internal/config"type Config struct {
DBURL string
RedisURL string
RabbitMQURL string
HTTPPort string
}func Load() (*Config, error)Why did i use "method muation" vs "factory functions" - ensure that the cfg now doesn't have any half upated config \n - its cleanr to init a var as a result of func calling -
import "adv-bknd/internal/domain"type User struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
CreatedAt time.Time `json:"created_at"`
}import "adv-bknd/internal/infrastructure"type RabbitMQClient struct {
// contains filtered or unexported fields
}func NewRabbitMQClient(url string) (*RabbitMQClient, error)func (r *RabbitMQClient) Close()func (r *RabbitMQClient) Consume() (<-chan amqp091.Delivery, error)func (r *RabbitMQClient) Publish(ctx context.Context, body []byte) errorhere i am publishing with context bcz currently the amqp lib doesn't impleemt syscall lv; interrrupts but has some preflight optimization in-place. it is so bcz amqp091, a fork of streadway/amqp, inherited the signature from its parent fut not yet fully implemeted it. In casein future, if it ever gets implemented, the code will already be ready with the nneded tools and can be updated hassle free
dfn of my client
type RedisClient struct {
// contains filtered or unexported fields
}func NewRedisClient(url string) (*RedisClient, error)redis connetion handshaker function - kept a retry duration of 5secs
func (r *RedisClient) Del(ctx context.Context, key string) errorfunc (r *RedisClient) Get(ctx context.Context, key string) (string, error)func (r *RedisClient) Set(ctx context.Context, key string, value any, exp time.Duration) errorimport "adv-bknd/internal/repository"i didn't directly use sql.DB everywhere bcz it is possiblr that at a later stage i would want to may be add a mutex which may break the app so just thought of keeping a low cost abstraction for future usability
type DB struct {
*sql.DB
}func NewDB(dburl string, migfs fs.FS) (*DB, error)inits the DB and makes the migs actually run
type UserRepository struct {
// contains filtered or unexported fields
}func NewUserRepository(db *DB) *UserRepositoryfunc (u *UserRepository) Create(ctx context.Context, user *domain.User) errorfunc (u *UserRepository) DeleteUser(ctx context.Context, id string) errorfunc (u *UserRepository) GetUserById(ctx context.Context, id string) (*domain.User, error)import "adv-bknd/internal/service"- type UserService
- func NewUserService(repo *repository.UserRepository, redis *infrastructure.RedisClient, rabbitmq *infrastructure.RabbitMQClient) *UserService
- func (u *UserService) DeleteUser(ctx context.Context, userID string) error
- func (u *UserService) GetUser(ctx context.Context, userID string) (*domain.User, error)
- func (u *UserService) Register(ctx context.Context, email string, passwd string) (*domain.User, error)
type UserService struct {
// contains filtered or unexported fields
}func NewUserService(repo *repository.UserRepository, redis *infrastructure.RedisClient, rabbitmq *infrastructure.RabbitMQClient) *UserServicefunc (u *UserService) DeleteUser(ctx context.Context, userID string) errorfunc (u *UserService) GetUser(ctx context.Context, userID string) (*domain.User, error)try for a cache hit if cache missed, go for a db fetch, but but but there is a chance that a cache-stampede may happen to avoid that, we are using SingleFlight here
func (u *UserService) Register(ctx context.Context, email string, passwd string) (*domain.User, error)say the rabbitmq crashed or smth wrong happens with it, it bare minimum the user creation surely takes place if th required contraints required for it s creations are satisfied
import "adv-bknd/internal/transport/http"- func LoggingMiddleware(next http.Handler) http.Handler
- func RecoveryMiddleware(next http.Handler) http.Handler
- type CreateUserRequest
- type Handler
- func NewHandler(service *service.UserService) *Handler
- func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request)
- func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request)
- func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request)
- func (h *Handler) RegisterRoutes(mux *http.ServeMux)
func LoggingMiddleware(next http.Handler) http.Handlerfunc RecoveryMiddleware(next http.Handler) http.Handlertype CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}type Handler struct {
// contains filtered or unexported fields
}func NewHandler(service *service.UserService) *Handlerfunc (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request)func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request)func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request)func (h *Handler) RegisterRoutes(mux *http.ServeMux)Generated by gomarkdoc