Skip to content

PunVas/golang-proj-adv-bknd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

User Management Service

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.


Architecture & Tech Stack

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

Key Features

1. Clean Architecture

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.

2. Performance Optimizations

  • 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.

3. Event-Driven (Async)

  • 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.

4. Graceful Shutdown

  • 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

Getting Started

Prerequisites

  • Docker & Docker Compose

Run the Stack

# Starts API, Worker, Postgres, Redis, RabbitMQ
docker-compose up --build

The API is now running on localhost:8080.


API Usage

1. Create User

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.

2. Get User (Test Caching)

#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}

3. Delete User

curl -X DELETE http://localhost:8080/users/{USER_ID}

Effect: Removes user from both PostgreSQL and Redis.


Simulation Testing

Verify Graceful Shutdown

  1. Navigate to cmd/worker/main.go.
  2. Uncomment the time.Sleep(10 * time.Second) line to simulate a slow task.
  3. Trigger a user creation.
  4. Immediately run docker stop <worker_container_id>.
  5. Observation: The container will NOT stop immediately. It will wait for the 10s task to finish, then log "Stopped Gracefully".

Directory Structure

.
├── 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

migrations

import "adv-bknd/migrations"

Index

Variables

var FS embed.FS

api

import "adv-bknd/cmd/api"

Index

worker

import "adv-bknd/cmd/worker"

Index

config

import "adv-bknd/internal/config"

Index

type Config

type Config struct {
    DBURL       string
    RedisURL    string
    RabbitMQURL string
    HTTPPort    string
}

func Load

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 -

domain

import "adv-bknd/internal/domain"

Index

type User

type User struct {
    ID           string    `json:"id"`
    Email        string    `json:"email"`
    PasswordHash string    `json:"-"`
    CreatedAt    time.Time `json:"created_at"`
}

infrastructure

import "adv-bknd/internal/infrastructure"

Index

type RabbitMQClient

type RabbitMQClient struct {
    // contains filtered or unexported fields
}

func NewRabbitMQClient

func NewRabbitMQClient(url string) (*RabbitMQClient, error)

func (*RabbitMQClient) Close

func (r *RabbitMQClient) Close()

func (*RabbitMQClient) Consume

func (r *RabbitMQClient) Consume() (<-chan amqp091.Delivery, error)

func (*RabbitMQClient) Publish

func (r *RabbitMQClient) Publish(ctx context.Context, body []byte) error

here 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

type RedisClient

dfn of my client

type RedisClient struct {
    // contains filtered or unexported fields
}

func NewRedisClient

func NewRedisClient(url string) (*RedisClient, error)

redis connetion handshaker function - kept a retry duration of 5secs

func (*RedisClient) Del

func (r *RedisClient) Del(ctx context.Context, key string) error

func (*RedisClient) Get

func (r *RedisClient) Get(ctx context.Context, key string) (string, error)

func (*RedisClient) Set

func (r *RedisClient) Set(ctx context.Context, key string, value any, exp time.Duration) error

repository

import "adv-bknd/internal/repository"

Index

type DB

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

func NewDB(dburl string, migfs fs.FS) (*DB, error)

inits the DB and makes the migs actually run

type UserRepository

type UserRepository struct {
    // contains filtered or unexported fields
}

func NewUserRepository

func NewUserRepository(db *DB) *UserRepository

func (*UserRepository) Create

func (u *UserRepository) Create(ctx context.Context, user *domain.User) error

func (*UserRepository) DeleteUser

func (u *UserRepository) DeleteUser(ctx context.Context, id string) error

func (*UserRepository) GetUserById

func (u *UserRepository) GetUserById(ctx context.Context, id string) (*domain.User, error)

service

import "adv-bknd/internal/service"

Index

type UserService

type UserService struct {
    // contains filtered or unexported fields
}

func NewUserService

func NewUserService(repo *repository.UserRepository, redis *infrastructure.RedisClient, rabbitmq *infrastructure.RabbitMQClient) *UserService

func (*UserService) DeleteUser

func (u *UserService) DeleteUser(ctx context.Context, userID string) error

func (*UserService) GetUser

func (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 (*UserService) Register

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

http

import "adv-bknd/internal/transport/http"

Index

func LoggingMiddleware

func LoggingMiddleware(next http.Handler) http.Handler

func RecoveryMiddleware

func RecoveryMiddleware(next http.Handler) http.Handler

type CreateUserRequest

type CreateUserRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

type Handler

type Handler struct {
    // contains filtered or unexported fields
}

func NewHandler

func NewHandler(service *service.UserService) *Handler

func (*Handler) CreateUser

func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request)

func (*Handler) DeleteUser

func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request)

func (*Handler) GetUser

func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request)

func (*Handler) RegisterRoutes

func (h *Handler) RegisterRoutes(mux *http.ServeMux)

Generated by gomarkdoc

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors