CQRS Architecture

Overview

We have implemented CQRS (Command Query Responsibility Segregation) to separate read and write operations for better performance and scalability.

Architecture

Write Side (Commands)

  • Purpose: Handle state changes (Create, Update, Delete)
  • Repository: UserRepositoryusers collection
  • Service: UserService
  • Handler: UserHandler
  • Operations:
    • Create User
    • Update User
    • Delete User

Read Side (Queries)

  • Purpose: Optimized for fast reads
  • Repository: UserReadRepositoryusers_read collection
  • Service: UserQueryService
  • Handler: UserQueryHandler
  • Operations:
    • Get User by ID
    • List Users (paginated)
    • List Users by Role (paginated)

Event Flow

1. User Registration (Command)

2. UserService.Create() → Save to `users` collection

3. Publish `UserCreated` event to NATS

4. UserConsumer receives event

5. Update `users_read` collection (Read Model)

Benefits

  1. Performance: Read operations don’t compete with writes
  2. Scalability: Can scale read and write databases independently
  3. Optimization: Read models are denormalized for fast queries
  4. Flexibility: Can have multiple read models for different use cases

API Endpoints

Write Operations (Commands)

# Create User
POST /api/v1/auth/register
{
  "email": "user@example.com",
  "password": "password123",
  "name": "John Doe",
  "roles": ["buyer"]
}

# Update User
PUT /api/v1/users/:id
{
  "name": "Jane Doe",
  "roles": ["buyer", "seller"]
}

# Delete User
DELETE /api/v1/users/:id

Read Operations (Queries)

# Get User by ID
GET /api/v1/users/:id

# List All Users (paginated)
GET /api/v1/users?limit=20&offset=0

# List Users by Role
GET /api/v1/users/by-role/buyer?limit=20&offset=0

Read Model Schema

The UserReadModel includes denormalized fields for fast queries:
type UserReadModel struct {
    ID        string    // Primary key
    Email     string    // Indexed for fast lookup
    Name      string
    Roles     []string
    IsActive  bool
    CreatedAt time.Time
    UpdatedAt time.Time

    // Denormalized fields
    RoleCount int      // Pre-calculated
    IsBuyer   bool     // Fast filtering
    IsSeller  bool     // Fast filtering
}

Testing the Implementation

1. Start the Application

go run cmd/server/main.go

2. Register a User (Write)

curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123",
    "name": "Test User",
    "roles": ["buyer"]
  }'

3. Login to Get Token

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }'

4. Query Users (Read)

# Get user by ID
curl -H "Authorization: Bearer YOUR_TOKEN" \
  http://localhost:8080/api/v1/users/USER_ID

# List all users
curl -H "Authorization: Bearer YOUR_TOKEN" \
  http://localhost:8080/api/v1/users?limit=10&offset=0

# List buyers
curl -H "Authorization: Bearer YOUR_TOKEN" \
  http://localhost:8080/api/v1/users/by-role/buyer?limit=10&offset=0

Monitoring

Check the logs to see the event flow:
INFO    UserCreated event received      {"user_id": "...", "email": "test@example.com"}
INFO    Read model updated successfully {"user_id": "..."}
INFO    Sending welcome email to user...

Next Steps

  1. Add More Events: UserUpdated, UserDeleted
  2. Implement Outbox Pattern: Guarantee event delivery
  3. Add Caching: Redis for frequently accessed read models
  4. Add Indexes: Optimize database queries
  5. Add Projections: Create specialized read models for different use cases