Skip to content

triunai/microservices-template-net8

Repository files navigation

Multi-Tenant POS Microservice Architecture Analysis

Project: MicroservicesBase
Target Domain: Point of Sale (POS) Systems
Date: October 7, 2025
Architecture Pattern: Clean Architecture + CQRS + Database-per-Tenant


πŸš€ Quick Start

New to this project? β†’ See SETUP.md for complete setup instructions!

TL;DR:

  1. Install .NET 8, SQL Server, Docker
  2. Run start-redis.bat (Windows) or start-redis.sh (Mac/Linux)
  3. Create databases (see SETUP.md for SQL scripts)
  4. Open in Visual Studio and press F5

πŸ“Š Feature Status (Quick Reference)

Category Status Details
Resilience βœ… DONE Circuit breakers, retry, timeout, pool sizing
Observability βœ… DONE Serilog, health checks, correlation IDs, request logging
API Features βœ… DONE Versioning, rate limiting, exception handling
Audit Logging βœ… DONE Background queue, compression, PII masking
Load Testing βœ… DONE k6 stampede + rate limit tests
Cache Warmup βœ… DONE Startup pre-warming, cold-start protection, dynamic tenant discovery
Mapping βœ… DONE Mapperly compile-time mappers, zero-overhead
Read Operations βœ… DONE GetSaleById with Polly resilience
Write Operations ❌ TODO CreateSale, VoidSale, RefundSale
Authentication ❌ TODO JWT, RBAC, tenant validation
Idempotency ⚠️ PARTIAL DB constraints only, need middleware
Migrations ⚠️ MANUAL Manual per-tenant, need orchestration

πŸ“‹ Table of Contents

  1. Feature Status
  2. Executive Summary
  3. Architecture Overview
  4. Implemented Enterprise Features
  5. Layer-by-Layer Analysis
  6. Multi-Tenancy Strategy
  7. Database Schema Analysis
  8. Strengths & Best Practices
  9. Areas for Improvement
  10. Testing & Load Testing
  11. Recommendations by Priority
  12. POS-Specific Considerations
  13. Conclusion

🎯 Executive Summary

This is a well-architected multi-tenant microservice template specifically designed for POS systems. The codebase demonstrates strong architectural principles with clean separation of concerns, modern .NET 8 patterns, and appropriate technology choices for high-performance transaction processing.

Key Architectural Decisions:

  • βœ… Database-per-tenant isolation (optimal for POS compliance & security)
  • βœ… CQRS with MediatR (scalable query/command separation)
  • βœ… FastEndpoints (high-performance HTTP API)
  • βœ… Dapper + Stored Procedures (optimal read performance)
  • βœ… FluentResults (functional error handling)
  • βœ… Vertical Slice Architecture (cohesive feature organization)

Current State: Enterprise-grade multi-tenant foundation with comprehensive resilience, observability, and security features. Production-ready for read operations. Ready for write operations and authentication.


πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 HTTP Client (POS Terminal)                   β”‚
β”‚           Header: X-Tenant: 7ELEVEN                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       MicroservicesBase.API                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ TenantResolutionMiddleware                             β”‚  β”‚
β”‚   β”‚  β€’ Extracts tenant from X-Tenant header                β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ FastEndpoints                                          β”‚  β”‚
β”‚   β”‚  β€’ GET /api/sales/{id}                                β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                MicroservicesBase.Infrastructure               β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ MediatR Query Handlers                                 β”‚  β”‚
β”‚   β”‚  β€’ GetSaleById.Handler                                 β”‚  β”‚
β”‚   β”‚  β€’ Inline FluentValidation                             β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚           β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ SalesReadDac (ISalesReadDac)                           β”‚  β”‚
β”‚   β”‚  β€’ Executes Dapper queries                             β”‚  β”‚
β”‚   β”‚  β€’ Calls stored procedures                             β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚           β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ MasterTenantConnectionFactory                           β”‚  β”‚
β”‚   β”‚  β€’ Resolves tenant β†’ connection string                  β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚                                         β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   TenantMaster DB   β”‚                   β”‚  Sales7Eleven DB    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚                   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Tenants       β”‚  β”‚                   β”‚  β”‚ Sales         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                   β”‚  β”‚ SaleItems     β”‚  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                               β”‚
                                               β”‚
                                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
                                      β”‚ SalesBurgerKing β”‚
                                      β”‚     DB          β”‚
                                      β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                                      β”‚ β”‚ Sales      β”‚  β”‚
                                      β”‚ β”‚ SaleItems  β”‚  β”‚
                                      β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Layer-by-Layer Analysis

1️⃣ MicroservicesBase.API (Presentation Layer)

Purpose: HTTP API endpoint exposure and tenant context resolution

Technology Stack:

  • FastEndpoints 7.0.1
  • Swagger/OpenAPI
  • ASP.NET Core 8.0
  • k6 (load and rate limit testing)

Key Components:

Program.cs

// Clean and minimal startup
- FastEndpoints registration
- Infrastructure DI registration
- TenantResolutionMiddleware
- Swagger documentation

Observations:

  • βœ… Minimal and focused
  • βœ… Authorization commented out (ready to implement)
  • ⚠️ No global exception handling middleware
  • ⚠️ ProblemDetails/ folder empty (RFC 7807 not implemented)

TenantResolutionMiddleware.cs

Responsibility: Extract tenant identifier from HTTP header

// Reads X-Tenant header
// Sets tenant context via HeaderTenantProvider

Strengths:

  • βœ… Simple and effective
  • βœ… Non-invasive middleware approach

Improvements Needed:

  • ⚠️ No validation if tenant exists
  • ⚠️ No handling of missing/invalid tenant
  • ⚠️ No logging of tenant resolution
  • ⚠️ Type cast to concrete HeaderTenantProvider (breaks ISP)

Endpoints/Sales/GetById/Endpoint.cs

Pattern: FastEndpoints vertical slice

public sealed class Endpoint(IMediator mediator) : EndpointWithoutRequest
{
    // Route: GET /api/sales/{id:guid}
    // Returns: SaleResponse or 404/400
}

Strengths:

  • βœ… Clean routing with type-safe parameters
  • βœ… Proper use of MediatR for query dispatch
  • βœ… Error handling with status codes
  • βœ… Anonymous access (ready for auth layer)

Improvements:

  • ⚠️ Manual error code mapping (consider result pattern helper)
  • ⚠️ Hard-coded error strings ("SALE_NOT_FOUND")

2️⃣ MicroservicesBase.Core (Domain Layer)

Purpose: Business logic, domain entities, abstractions, and contracts

Dependencies: NONE (pure domain layer βœ…)

Domain Entities

Sale.cs
public sealed class Sale
{
    // Tenant context
    - TenantId, StoreId, RegisterId
    
    // Business data
    - ReceiptNumber, CreatedAt
    - NetTotal, TaxTotal, GrandTotal
    - Items (SaleItem collection)
    
    // Factory method
    + Sale.Create(...)
    
    // Behavior
    + ApplyTax(decimal)
    - RecomputeTotals() (private encapsulation)
}

Strengths:

  • βœ… Encapsulation with private setters
  • βœ… Factory method prevents invalid construction
  • βœ… Computed properties (GrandTotal)
  • βœ… Private parameterless constructor for ORM
  • βœ… Multi-store, multi-register aware

Observations:

  • ⚠️ Doesn't inherit from Entity base class (unused abstraction)
  • ⚠️ Money value object exists but not used (using decimal directly)
  • ⚠️ No discount support yet (commented in code)
  • ⚠️ No payment tracking
  • ⚠️ Tax calculation is placeholder (needs strategy pattern)
SaleItem.cs
public sealed class SaleItem
{
    - Sku, Qty, UnitPrice
    - Subtotal (computed)
    + SaleItem.Create(...)
}

Strengths:

  • βœ… Immutable once created
  • βœ… Computed subtotal property
  • βœ… Factory method

Improvements:

  • ⚠️ No product name/description
  • ⚠️ No discount per line item
  • ⚠️ No tax rate per item (for tax calculation)
Money.cs
public sealed record Money(decimal Amount, string Currency)
{
    + Zero(currency)
    + EnsureNonNegative()
    + Add(Money other)
}

Observation:

  • βœ… Good value object implementation
  • ❌ NOT USED anywhere in domain (consider using or removing)
Entity.cs
public abstract class Entity
{
    - Id (Guid)
    - Equality by Id
}

Observation:

  • βœ… Proper identity equality
  • ❌ NOT USED by Sale entity (consider using or removing)

Abstractions (Interfaces)

ITenantProvider
public interface ITenantProvider
{
    string? Id { get; }
}

Purpose: Provide current tenant context (scoped per request)

ITenantConnectionFactory
public interface ITenantConnectionFactory
{
    Task<string> GetSqlConnectionStringAsync(string tenantId, CancellationToken ct);
}

Purpose: Resolve tenant ID β†’ SQL connection string

ISalesReadDac
public interface ISalesReadDac
{
    Task<SaleReadModel?> GetByIdAsync(Guid saleId, CancellationToken ct);
}

Purpose: Data access abstraction for read operations

Observation:

  • βœ… Separate read models (SaleReadModel) from domain entities
  • βœ… Clean dependency inversion

Constants

- StoredProcedureNames.cs (dbo.GetSaleWithItems)
- ErrorMessage.cs (generic error messages)
- AuthIdentityErrorMessage.cs
- UserErrorMessage.cs
- RegexPattern.cs
- File.cs

Strengths:

  • βœ… Centralized constants (no magic strings)
  • βœ… Stored procedure names in one place

Utilities (JSON Converters)

- DecimalPrecisionConverter.cs (critical for financial precision!)
- JsonDateTimeOffsetConverter.cs
- NullToDefaultConverter.cs
- TrimmingConverter.cs

Observation:

  • βœ… DecimalPrecisionConverter is critical for POS (prevents rounding errors)

3️⃣ MicroservicesBase.Infrastructure (Application + Persistence Layer)

Purpose: Implementation of abstractions, persistence, queries, commands

Technology Stack:

  • Dapper 2.1.66
  • MediatR 13.0.0
  • FluentValidation 12.0.0
  • FluentResults 4.0.0
  • Microsoft.Data.SqlClient 6.1.1

Tenancy Implementation

HeaderTenantProvider.cs
public sealed class HeaderTenantProvider : ITenantProvider
{
    public string? Id { get; private set; }
    public void SetTenant(string tenant) => Id = tenant;
}

Registered as: Scoped (per-request)

Strengths:

  • βœ… Simple stateful service
  • βœ… Scoped lifetime ensures isolation

Improvements:

  • ⚠️ Mutable state (consider making immutable)
  • ⚠️ No validation of tenant ID format
MasterTenantConnectionFactory.cs
public sealed class MasterTenantConnectionFactory : ITenantConnectionFactory
{
    // Queries TenantMaster.dbo.Tenants
    // Returns connection string for given tenant
    // Throws if tenant not found
}

Registered as: Singleton

Strengths:

  • βœ… Central tenant registry
  • βœ… Dynamic tenant resolution (no hardcoded strings)
  • βœ… Console logging for debugging

Critical Improvements Needed:

  • πŸ”΄ NO CACHING (queries master DB on every request!)
  • ⚠️ No circuit breaker for master DB failures
  • ⚠️ Exception thrown on invalid tenant (should return error)
  • ⚠️ No support for inactive tenants

Persistence Layer

SalesReadDac.cs
public sealed class SalesReadDac(ITenantConnectionFactory connFactory, ITenantProvider tenant)
    : ISalesReadDac
{
    // Uses stored procedure: dbo.GetSaleWithItems
    // Returns head row + item rows via multi-result set
    // Maps to SaleReadModel
}

Strengths:

  • βœ… Proper dependency injection
  • βœ… Async/await throughout
  • βœ… Stored procedure for performance
  • βœ… QueryMultipleAsync for efficient data retrieval
  • βœ… Private mapping records (_Head, _Item)

Observations:

  • βœ… NOLOCK hints (acceptable for POS read queries)
  • ⚠️ No error handling for connection failures
  • ⚠️ No retry policy for transient errors

CQRS Implementation

GetSaleById.cs
public class GetSaleById
{
    // 1. Query record
    public sealed record Query(Guid SaleId) : IRequest<Result<SaleResponse>>;
    
    // 2. Validator
    public sealed class Validator : AbstractValidator<Query>
    
    // 3. Handler
    public sealed class Handler : IRequestHandler<Query, Result<SaleResponse>>
}

Pattern: All query concerns in one file (vertical slice)

Strengths:

  • βœ… Self-contained feature (easy to navigate)
  • βœ… Inline validation (no separate pipeline)
  • βœ… FluentResults for error handling
  • βœ… Error codes for client consumption ("SALE_NOT_FOUND")
  • βœ… Mapping from read model β†’ response contract

Observations:

  • βœ… Validation is simple (just NotEmpty on ID)
  • ⚠️ Validation errors return array but only first is used
  • ⚠️ Manual mapping (consider AutoMapper or Mapperly)

Dependency Registration

Extensions.cs
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
    // MediatR assembly scanning
    // Singleton: ITenantConnectionFactory
    // Scoped: ISalesReadDac, ITenantProvider
}

Strengths:

  • βœ… Clean extension method
  • βœ… Proper service lifetimes

4️⃣ MicroservicesBase.Schedulers (Background Jobs)

Status: Placeholder ("Hello, World!")

Future Use Cases:

  • Scheduled reports
  • Data archiving
  • Tenant database migrations
  • Batch synchronization

πŸ” Multi-Tenancy Strategy

Chosen Approach: Database-per-Tenant

Architecture:

TenantMaster DB
  └── Tenants Table (Name β†’ ConnectionString mapping)

Tenant-Specific DBs (identical schema)
  β”œβ”€β”€ Sales7Eleven
  β”œβ”€β”€ SalesBurgerKing
  └── [Future Tenants...]

Pros for POS Systems:

βœ… Complete data isolation (security & compliance)
βœ… Per-tenant performance tuning (indexes, partitioning)
βœ… Easy backup/restore per tenant
βœ… Tenant-specific scaling (can move to different servers)
βœ… Regulatory compliance (GDPR, PCI-DSS data residency)
βœ… Schema customization possible per tenant

Cons:

❌ Cross-tenant analytics require federation
❌ Schema migrations must run against all DBs
❌ Higher operational overhead
❌ More expensive (more databases to maintain)

Verdict for POS: βœ… Correct choice!

  • POS systems prioritize data isolation
  • Regulatory compliance is critical
  • Each store/franchise is fully isolated

πŸ“Š Database Schema Analysis

TenantMaster Database

CREATE TABLE dbo.Tenants(
  Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
  Name NVARCHAR(100) NOT NULL UNIQUE,           -- Tenant identifier
  ConnectionString NVARCHAR(500) NOT NULL,      -- Full SQL connection string
  IsActive BIT NOT NULL DEFAULT(1)              -- Soft disable
);

Observations:

  • βœ… GUID primary key (good for distributed systems)
  • βœ… Unique constraint on Name (prevents duplicates)
  • βœ… IsActive flag for soft deletion
  • βœ… Connection string stored directly (simple approach)

Security Concerns:

  • πŸ”΄ Connection strings in plain text (consider encryption at rest)
  • ⚠️ No audit fields (CreatedAt, ModifiedAt)
  • ⚠️ No tenant metadata (contact info, billing tier, etc.)

Recommended Fields to Add:

- CreatedAt DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET()
- ModifiedAt DATETIMEOFFSET
- CreatedBy NVARCHAR(100)
- BillingTier NVARCHAR(50)  -- for rate limiting/features
- MaxStores INT             -- tenant limits
- MaxRegisters INT

Tenant Database Schema

Sales Table

CREATE TABLE dbo.Sales (
  Id UNIQUEIDENTIFIER PRIMARY KEY,
  TenantId NVARCHAR(100) NOT NULL,              -- Redundant but useful for validation
  StoreId NVARCHAR(50) NOT NULL,                -- Multi-store support
  RegisterId NVARCHAR(50) NOT NULL,             -- Multi-register support
  ReceiptNumber NVARCHAR(50) NOT NULL,          -- Human-readable identifier
  CreatedAt DATETIMEOFFSET NOT NULL,            -- Timestamp with timezone
  NetTotal DECIMAL(18,2) NOT NULL,              -- Subtotal before tax
  TaxTotal DECIMAL(18,2) NOT NULL,              -- Tax amount
  GrandTotal DECIMAL(18,2) NOT NULL             -- Final total
);

-- Prevent duplicate receipts per register
CREATE UNIQUE INDEX UX_Sales_Receipt ON dbo.Sales(StoreId, RegisterId, ReceiptNumber);

Strengths:

  • βœ… GUID primary key (globally unique, good for sync)
  • βœ… TenantId redundancy (defense in depth)
  • βœ… StoreId + RegisterId (multi-location ready)
  • βœ… Unique constraint on receipt number (idempotency)
  • βœ… DATETIMEOFFSET for timezone awareness
  • βœ… DECIMAL(18,2) for financial precision

Missing Fields (Typical POS Requirements):

- Status NVARCHAR(20)          -- 'COMPLETED', 'VOIDED', 'REFUNDED'
- CashierId NVARCHAR(50)       -- Who processed the sale
- CustomerId UNIQUEIDENTIFIER  -- If loyalty program
- PaymentMethod NVARCHAR(50)   -- 'CASH', 'CARD', 'MOBILE'
- DiscountTotal DECIMAL(18,2)  -- Promotions/coupons
- ChangeDue DECIMAL(18,2)      -- For cash transactions
- VoidedAt DATETIMEOFFSET      -- Audit trail
- VoidedBy NVARCHAR(50)
- RefundedAt DATETIMEOFFSET
- Notes NVARCHAR(MAX)          -- Manager override reasons

Recommended Indexes:

-- Performance indexes for typical POS queries
CREATE INDEX IX_Sales_CreatedAt ON dbo.Sales(CreatedAt DESC);
CREATE INDEX IX_Sales_Store_Created ON dbo.Sales(StoreId, CreatedAt DESC);
CREATE INDEX IX_Sales_Receipt ON dbo.Sales(ReceiptNumber);

SaleItems Table

CREATE TABLE dbo.SaleItems (
  Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
  SaleId UNIQUEIDENTIFIER NOT NULL REFERENCES dbo.Sales(Id),
  Sku NVARCHAR(50) NOT NULL,
  Qty INT NOT NULL,
  UnitPrice DECIMAL(18,2) NOT NULL
);

Strengths:

  • βœ… Foreign key constraint (referential integrity)
  • βœ… Simple line item structure

Missing Fields:

- ProductName NVARCHAR(200)       -- Snapshot (product names change)
- DiscountAmount DECIMAL(18,2)    -- Line-level discounts
- TaxRate DECIMAL(5,2)            -- For tax calculation auditing
- TaxAmount DECIMAL(18,2)         -- Line-level tax
- Category NVARCHAR(50)           -- For reporting
- IsRefunded BIT                  -- Partial refund tracking

Recommended Indexes:

CREATE INDEX IX_SaleItems_SaleId ON dbo.SaleItems(SaleId);
CREATE INDEX IX_SaleItems_Sku ON dbo.SaleItems(Sku);  -- Product analytics

Stored Procedure Analysis

dbo.GetSaleWithItems

CREATE PROCEDURE dbo.GetSaleWithItems @SaleId UNIQUEIDENTIFIER AS
BEGIN
  SET NOCOUNT ON;
  
  -- Result set 1: Header
  SELECT Id,TenantId,StoreId,RegisterId,ReceiptNumber,CreatedAt,NetTotal,TaxTotal,GrandTotal
  FROM dbo.Sales WITH (NOLOCK) WHERE Id = @SaleId;

  -- Result set 2: Items
  SELECT Sku,Qty,UnitPrice
  FROM dbo.SaleItems WITH (NOLOCK) WHERE SaleId = @SaleId;
END

Strengths:

  • βœ… SET NOCOUNT ON (performance best practice)
  • βœ… Multiple result sets (efficient single round-trip)
  • βœ… NOLOCK hints (acceptable for read-heavy POS queries)

Observations:

  • ⚠️ NOLOCK can cause dirty reads (acceptable for POS dashboard, not for financial reports)
  • ⚠️ No error handling
  • ⚠️ No existence check (returns empty set if not found)

Recommended Enhancement:

-- Add parameter validation
IF @SaleId IS NULL
  THROW 50001, 'SaleId cannot be null', 1;

-- Consider READ COMMITTED SNAPSHOT isolation instead of NOLOCK

Seed Data

-- Tenant: 7-Eleven
INSERT dbo.Sales(Id,TenantId,StoreId,RegisterId,ReceiptNumber,CreatedAt,NetTotal,TaxTotal,GrandTotal)
VALUES ('11111111-1111-1111-1111-111111111111','7ELEVEN','STORE001','REG01','RCP-7E-001',
        SYSDATETIMEOFFSET(),20.00,1.20,21.20);

INSERT dbo.SaleItems(SaleId,Sku,Qty,UnitPrice)
VALUES ('11111111-1111-1111-1111-111111111111','SKU-COFFEE',1,10.00),
       ('11111111-1111-1111-1111-111111111111','SKU-SANDWICH',1,10.00);

Observation:

  • βœ… Test data present (good for development)
  • ⚠️ Only 7-Eleven has data (no BurgerKing seed data)

βœ… Strengths & Best Practices

Architecture

  1. βœ… Clean Architecture - proper layer separation
  2. βœ… CQRS - read/write separation ready
  3. βœ… Vertical Slices - features organized cohesively
  4. βœ… Dependency Inversion - abstractions in Core layer
  5. βœ… Domain-Driven Design - rich domain models with behavior

Technology Choices

  1. βœ… FastEndpoints - high performance, type-safe routing
  2. βœ… Dapper - optimal performance for data access
  3. βœ… Stored Procedures - database-level optimization
  4. βœ… FluentResults - functional error handling
  5. βœ… MediatR - decoupled request/response handling

Multi-Tenancy

  1. βœ… Database-per-tenant - correct choice for POS security/compliance
  2. βœ… Dynamic tenant resolution - no hardcoded connection strings in code
  3. βœ… Middleware-based tenant context - non-invasive

Code Quality

  1. βœ… Nullable reference types enabled
  2. βœ… Record types for DTOs (immutability)
  3. βœ… Primary constructors (modern C# 12 syntax)
  4. βœ… Factory methods prevent invalid domain state
  5. βœ… Encapsulation with private setters and computed properties
  6. βœ… Constants centralization - no magic strings

POS-Specific

  1. βœ… Decimal precision converters - critical for financial accuracy
  2. βœ… Multi-store, multi-register support - enterprise ready
  3. βœ… Receipt number uniqueness - idempotency enforced at DB level
  4. βœ… Timezone-aware timestamps - DATETIMEOFFSET usage

βœ… Implemented Enterprise Features (NEW!)

Resilience & Performance

  1. βœ… Polly v8 Circuit Breakers - Failure-ratio based circuit breakers for MasterDb, TenantDb, Redis, AuditDb
  2. βœ… Retry Policies - Exponential backoff with jitter, transient error classification
  3. βœ… Timeout Enforcement - 700ms MasterDb, 1500ms TenantDb, 200ms Redis
  4. βœ… Connection String Caching - IMemoryCache with 10-min TTL, reduces MasterDb load by 99%+
  5. βœ… SQL Connection Pooling - 200 max connections per tenant (prevents exhaustion)
  6. βœ… Stampede Protection - IMemoryCache.GetOrCreateAsync thread-safe factory
  7. βœ… Cache Warmup on Startup - IHostedService pre-warms all active tenants, eliminates cold-start delays

Observability & Monitoring

  1. βœ… Structured Logging (Serilog) - Correlation ID + Tenant ID enrichment, async/non-blocking
  2. βœ… Health Checks - SQL Server + Redis endpoints (/health/ready, /health/live)
  3. βœ… Request Logging - Duration tracking, tenant context, correlation tracking
  4. βœ… Polly Logging Callbacks - Circuit breaker state changes, retry attempts, timeouts

Security & Error Handling

  1. βœ… Global Exception Handler - RFC 7807 ProblemDetails for all errors
  2. βœ… Consistent Error Responses - 400/404/500 with correlation ID, tenant ID, trace ID
  3. βœ… Route Constraint Validation - GUID validation, bad request handling
  4. βœ… 404 β†’ 400 Middleware - Aggressive invalid endpoint rejection

API Features

  1. βœ… API Versioning - URL-based (v1) + header-based versioning
  2. βœ… Rate Limiting - Per-tenant sliding window (1000 req/10s), custom headers (X-RateLimit-Limit, Retry-After)
  3. βœ… Correlation ID Middleware - Request tracing across logs
  4. βœ… Tenant Resolution Middleware - X-Tenant header extraction

Audit & Compliance

  1. βœ… Background Audit Logging - Bounded queue (10k capacity), batch writes (200 items)
  2. βœ… Payload Compression - Gzip request/response data
  3. βœ… PII Masking - Email, phone, cardNumber, password, token
  4. βœ… Per-Tenant Storage - AuditLog table in each tenant DB
  5. βœ… Configurable Sampling - Separate rates for reads/writes

Code Quality

  1. βœ… Constants Refactoring - HttpConstants, SqlConstants, ResilienceConstants, CacheConstants, File.Mime
  2. βœ… CommandTimeout Alignment - Dapper timeout < Polly timeout (prevents hanging)
  3. βœ… Cancellation Token Propagation - All async operations support cancellation

Mapping & Data Transfer

  1. βœ… Mapperly Integration - Compile-time source generators for zero-overhead mapping
  2. βœ… Type-Safe Mappings - SaleReadModel β†’ SaleResponse with compile-time verification
  3. βœ… Enhanced Validation - Production-ready FluentValidation rules (GUID empty checks, error codes)

Load Testing

  1. βœ… k6 Stampede Test - 200 req/s spike load, multi-tenant, custom POS-focused summary
  2. βœ… k6 Rate Limit Test - Per-tenant isolation verification, multi-scenario
  3. βœ… Beautiful Test Output - Automated pass/fail verdicts, troubleshooting tips

⚠️ Areas for Improvement (UPDATED)

Critical (Must Fix)

  1. πŸ”΄ No authentication/authorization - API is completely open (JWT, RBAC, tenant claim validation needed)
  2. πŸ”΄ No tenant validation - Accepts any tenant header value (should validate against TenantMaster)
  3. πŸ”΄ Plain-text connection strings - Security risk (consider Azure Key Vault or IDataProtection)

High Priority

  1. ⚠️ No write operations (Commands) - Read-only system currently (CreateSale, VoidSale, RefundSale needed)
  2. ⚠️ No idempotency middleware - Database has unique constraints (basic protection), need Idempotency-Key header handling
  3. ⚠️ No payment tracking - Incomplete POS domain model (payments, refunds, voids needed)
  4. ⚠️ Tax calculation is placeholder - No real business logic (need strategy pattern)
  5. ⚠️ No multi-tenant migrations - Manual script execution per tenant (need DbUp or FluentMigrator orchestration)

Medium Priority

  1. ⚠️ Unused abstractions - Entity base class, Money value object (use or remove)
  2. ⚠️ Manual error mapping - Repetitive code in endpoints (consider result pattern helper)
  3. ⚠️ Empty folders - Commands/, Mapping/ not implemented (Observability is now done!)
  4. ⚠️ LocalDB performance - 900ms queries under load (acceptable for dev, use SQL Server for production)

Low Priority

  1. ⚠️ No integration tests - only manual testing possible
  2. ⚠️ No Docker support - deployment not containerized
  3. ⚠️ No CI/CD configuration - manual deployment
  4. ⚠️ Missing database migration strategy - how to update all tenant DBs
  5. ⚠️ Console.WriteLine logging - production-ready logging needed

🎯 Recommendations by Priority

Phase 1: Security & Stability (Critical)

1. Implement Connection String Caching

// Add distributed cache (Redis) or in-memory cache
public async Task<string> GetSqlConnectionStringAsync(string tenantId, CancellationToken ct)
{
    return await _cache.GetOrCreateAsync($"tenant:{tenantId}", async entry => 
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        return await QueryMasterDbAsync(tenantId, ct);
    });
}

Impact: Reduces master DB load by 99%+

2. Add Authentication & Authorization

// JWT authentication with tenant claims
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { ... });

// Validate tenant in JWT matches X-Tenant header
[Authorize(Policy = "TenantAccess")]

Features Needed:

  • JWT token generation/validation
  • Role-based access (Cashier, Manager, Admin)
  • Tenant claim validation
  • API key support for POS terminals

3. Tenant Validation Middleware

public async Task InvokeAsync(HttpContext context)
{
    var tenant = context.Request.Headers["X-Tenant"].FirstOrDefault();
    
    if (string.IsNullOrWhiteSpace(tenant))
        return Results.Problem("X-Tenant header required", statusCode: 400);
    
    // Validate tenant exists and is active
    if (!await _tenantValidator.IsValidAsync(tenant))
        return Results.Problem("Invalid tenant", statusCode: 403);
    
    // ... continue
}

4. Global Exception Handling

app.UseExceptionHandler(appBuilder =>
{
    appBuilder.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        
        // Log exception with tenant context
        logger.LogError(exception, "Unhandled exception for tenant {TenantId}", tenantId);
        
        // Return RFC 7807 Problem Details
        var problem = new ProblemDetails
        {
            Status = 500,
            Title = "Internal Server Error",
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
        };
        
        await context.Response.WriteAsJsonAsync(problem);
    });
});

5. Encrypt Connection Strings

-- Use SQL Server encryption or Azure Key Vault
ALTER TABLE dbo.Tenants ADD ConnectionStringEncrypted VARBINARY(MAX);

-- Application decrypts using IDataProtection
var decrypted = _dataProtector.Unprotect(encrypted);

Phase 2: Core Features (High Priority)

6. Implement Write Operations (Commands)

Commands to Implement:

// MicroservicesBase.Infrastructure/Commands/Sales/
- CreateSale.cs
  - Command(TenantId, StoreId, RegisterId, Items[], PaymentMethod)
  - Validator (validate items, prices, store exists)
  - Handler (insert Sale + SaleItems in transaction)
  
- VoidSale.cs
  - Command(SaleId, Reason, VoidedBy)
  - Handler (soft delete, audit trail)
  
- RefundSale.cs
  - Command(SaleId, Items[], RefundMethod)
  - Handler (create negative sale, link to original)

Database Changes:

-- Add to Sales table
ALTER TABLE dbo.Sales ADD 
  Status NVARCHAR(20) NOT NULL DEFAULT 'COMPLETED',
  VoidedAt DATETIMEOFFSET NULL,
  VoidedBy NVARCHAR(100) NULL,
  VoidReason NVARCHAR(500) NULL;

-- Add stored procedure
CREATE PROCEDURE dbo.CreateSale
  @Id UNIQUEIDENTIFIER,
  @TenantId NVARCHAR(100),
  -- ... parameters
AS BEGIN
  BEGIN TRANSACTION;
  -- Insert Sale
  -- Insert SaleItems
  COMMIT TRANSACTION;
END

7. Implement Observability

Structured Logging (Serilog):

builder.Host.UseSerilog((context, config) =>
{
    config
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .Enrich.WithProperty("Application", "MicroservicesBase")
        .WriteTo.Console()
        .WriteTo.ApplicationInsights();
});

// Log enrichment with tenant context
LogContext.PushProperty("TenantId", tenantProvider.Id);

OpenTelemetry (Distributed Tracing):

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddSqlClientInstrumentation()
        .AddSource("MicroservicesBase"));

Metrics:

// Track tenant-specific metrics
var salesCounter = meter.CreateCounter<long>("sales.completed");
var salesRevenue = meter.CreateHistogram<decimal>("sales.revenue");

8. Add Retry Policies (Polly)

builder.Services.AddHttpClient<ITenantConnectionFactory, MasterTenantConnectionFactory>()
    .AddTransientHttpErrorPolicy(policy => 
        policy.WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

// SQL retry for transient errors
var retryPolicy = Policy
    .Handle<SqlException>(ex => /* transient error codes */)
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

9. Idempotency Support

// Endpoint receives Idempotency-Key header
var idempotencyKey = context.Request.Headers["Idempotency-Key"].FirstOrDefault();

// Store in cache or DB
if (await _idempotencyStore.ExistsAsync(idempotencyKey))
    return await _idempotencyStore.GetResponseAsync(idempotencyKey);

// Process request
var response = await ProcessAsync(...);

// Cache response
await _idempotencyStore.StoreAsync(idempotencyKey, response, TimeSpan.FromHours(24));

Database Approach:

CREATE TABLE dbo.IdempotencyKeys (
  Key NVARCHAR(100) PRIMARY KEY,
  Response NVARCHAR(MAX),
  CreatedAt DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET()
);
CREATE INDEX IX_IdempotencyKeys_CreatedAt ON dbo.IdempotencyKeys(CreatedAt);

10. Payment Tracking

// Add Payment entity
public sealed class Payment
{
    Guid Id, Guid SaleId, PaymentMethod Method,
    decimal Amount, string TransactionId, DateTimeOffset ProcessedAt
}
CREATE TABLE dbo.Payments (
  Id UNIQUEIDENTIFIER PRIMARY KEY,
  SaleId UNIQUEIDENTIFIER NOT NULL REFERENCES dbo.Sales(Id),
  Method NVARCHAR(50) NOT NULL,  -- CASH, CREDIT_CARD, DEBIT_CARD, MOBILE
  Amount DECIMAL(18,2) NOT NULL,
  TransactionId NVARCHAR(100),   -- External payment processor ID
  ProcessedAt DATETIMEOFFSET NOT NULL,
  CardLastFour NCHAR(4),         -- PCI compliance (never store full card)
  Status NVARCHAR(20) NOT NULL   -- AUTHORIZED, CAPTURED, REFUNDED
);

11. Audit Trail

// Add audit interceptor for all commands
public class AuditInterceptor<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        // Log request
        await _auditLog.WriteAsync(new AuditEntry
        {
            TenantId = _tenantProvider.Id,
            Action = typeof(TRequest).Name,
            UserId = _userContext.UserId,
            Timestamp = DateTimeOffset.UtcNow,
            RequestData = JsonSerializer.Serialize(request)
        });
        
        var response = await next();
        
        // Log response
        return response;
    }
}
CREATE TABLE dbo.AuditLog (
  Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
  TenantId NVARCHAR(100) NOT NULL,
  UserId NVARCHAR(100) NOT NULL,
  Action NVARCHAR(100) NOT NULL,
  EntityType NVARCHAR(100),
  EntityId UNIQUEIDENTIFIER,
  OldValue NVARCHAR(MAX),
  NewValue NVARCHAR(MAX),
  Timestamp DATETIMEOFFSET NOT NULL,
  IpAddress NVARCHAR(50)
);
CREATE INDEX IX_AuditLog_TenantId_Timestamp ON dbo.AuditLog(TenantId, Timestamp DESC);

12. Tax Calculation Strategy

public interface ITaxCalculator
{
    Task<decimal> CalculateTaxAsync(Sale sale, CancellationToken ct);
}

public class USTaxCalculator : ITaxCalculator
{
    // Query tax rate by StoreId (location-based)
    // Apply to eligible items
    // Handle tax-exempt items
}

public class EUVATCalculator : ITaxCalculator
{
    // VAT calculation logic
}

// Register based on tenant configuration
services.AddScoped<ITaxCalculator>(sp =>
{
    var config = sp.GetRequiredService<ITenantConfigProvider>();
    return config.TaxRegion switch
    {
        "US" => new USTaxCalculator(),
        "EU" => new EUVATCalculator(),
        _ => throw new NotSupportedException()
    };
});

Phase 3: Production Readiness (Medium Priority)

13. Clean Up Unused Abstractions

Option A: Use them

// Make Sale inherit from Entity
public sealed class Sale : Entity
{
    // Remove duplicate Id property
}

// Use Money value object
public Money NetTotal { get; private set; }
public Money TaxTotal { get; private set; }
public Money GrandTotal { get; private set; }

Option B: Remove them

  • Delete Entity.cs if not needed
  • Delete Money.cs if sticking with decimal

14. Health Checks

builder.Services.AddHealthChecks()
    .AddCheck<TenantDatabaseHealthCheck>("tenant_databases")
    .AddCheck<MasterDatabaseHealthCheck>("master_database");

app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready");
public class TenantDatabaseHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken ct)
    {
        // Check all active tenant DBs
        var tenants = await _tenantService.GetActiveTenants();
        foreach (var tenant in tenants)
        {
            try
            {
                await using var conn = new SqlConnection(tenant.ConnectionString);
                await conn.OpenAsync(ct);
            }
            catch (Exception ex)
            {
                return HealthCheckResult.Degraded($"Tenant {tenant.Name} database unavailable", ex);
            }
        }
        return HealthCheckResult.Healthy();
    }
}

15. Rate Limiting

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("per-tenant", context =>
    {
        var tenant = context.Request.Headers["X-Tenant"].FirstOrDefault();
        return RateLimitPartition.GetTokenBucketLimiter(tenant ?? "anonymous", _ =>
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                TokensPerPeriod = 100
            });
    });
});

app.UseRateLimiter();

16. API Versioning

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

// Endpoint
Get("/api/v1/sales/{id:guid}");

17. Validation with FastEndpoints

public sealed class Request
{
    public Guid Id { get; set; }
}

public sealed class Validator : Validator<Request>
{
    public Validator()
    {
        RuleFor(x => x.Id).NotEmpty();
    }
}

public sealed class Endpoint : Endpoint<Request, SaleResponse>
{
    // FastEndpoints auto-validates before HandleAsync
}

18. Implement Commands/, Observability/, Mapping/

Commands: See Phase 2 #6
Observability: See Phase 2 #7
Mapping: Consider Mapperly (source generator, zero-overhead)

[Mapper]
public partial class SalesMapper
{
    public partial SaleResponse ToResponse(SaleReadModel model);
}

Phase 4: DevOps & Testing (Low Priority)

19. Integration Tests

public class SalesEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetSaleById_ValidId_Returns200()
    {
        // Arrange
        var client = _factory.CreateClient();
        client.DefaultRequestHeaders.Add("X-Tenant", "7ELEVEN");
        
        // Act
        var response = await client.GetAsync("/api/sales/11111111-1111-1111-1111-111111111111");
        
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
}

20. Docker Support

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
COPY . .
RUN dotnet restore
RUN dotnet build -c Release

FROM build AS publish
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MicroservicesBase.API.dll"]
# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "5000:80"
    environment:
      ConnectionStrings__TenantMaster: "Server=sqlserver;Database=TenantMaster;..."
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: Y
      SA_PASSWORD: YourStrong!Passw0rd

21. CI/CD Pipeline

# .github/workflows/build.yml
name: Build and Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0.x'
      - run: dotnet restore
      - run: dotnet build --no-restore
      - run: dotnet test --no-build --verbosity normal

22. Database Migration Strategy

Option A: DbUp / Fluent Migrator

// On startup, apply migrations to all tenant DBs
var tenants = await _tenantService.GetAllAsync();
foreach (var tenant in tenants)
{
    var upgrader = DeployChanges.To
        .SqlDatabase(tenant.ConnectionString)
        .WithScriptsFromFileSystem("./Migrations")
        .Build();
    
    upgrader.PerformUpgrade();
}

Option B: Manual Script Runner

// API endpoint for admins
POST /api/admin/migrate-tenants
{
    "scriptName": "V2_AddPaymentsTable.sql"
}

πŸ›’ POS-Specific Considerations

Offline-First Architecture

POS terminals often lose internet connectivity. Consider:

  • Local SQLite database for offline transactions
  • Background sync service to push to central DB
  • Conflict resolution strategy (last-write-wins vs. CRDT)

Receipt Printing

public interface IReceiptPrinter
{
    Task<byte[]> GenerateReceiptAsync(Sale sale);  // PDF/ESC-POS format
}

Hardware Integration

  • Cash Drawer - serial/USB commands to open
  • Barcode Scanner - USB HID or serial input
  • Card Reader - PCI-compliant payment terminal integration
  • Receipt Printer - ESC/POS protocol

Performance Requirements

  • Sub-second response times for sale creation
  • Concurrent transactions from multiple registers
  • High availability during peak hours (lunch rush)

Inventory Management

public interface IInventoryService
{
    Task<bool> ReserveStockAsync(string sku, int quantity, CancellationToken ct);
    Task CommitReservationAsync(Guid reservationId, CancellationToken ct);
    Task RollbackReservationAsync(Guid reservationId, CancellationToken ct);
}

Promotions & Discounts

public interface IPromotionEngine
{
    Task<IEnumerable<Discount>> ApplyPromotionsAsync(Sale sale, CancellationToken ct);
}

// Types of promotions
- Buy X Get Y Free
- Percentage discount
- Fixed amount discount
- Bulk discount (tiered pricing)
- Time-based (happy hour)
- Coupon codes
- Loyalty points redemption

Reporting Requirements

// End-of-day report per register
public record DailyRegisterReport(
    string RegisterId,
    DateOnly Date,
    int TransactionCount,
    decimal TotalSales,
    decimal TotalTax,
    decimal CashSales,
    decimal CardSales,
    decimal Voids,
    decimal Refunds
);

// X-Report (mid-shift, no cash drawer reconciliation)
// Z-Report (end-of-shift, closes register)

Compliance & Security

  • PCI-DSS - never store full card numbers
  • GDPR - customer data protection
  • Tax authority integration - submit sales to government portals
  • Receipt archival - legal requirement (5-7 years)

Multi-Currency Support

For franchises operating in multiple countries:

public record Money(decimal Amount, string Currency);

public interface ICurrencyConverter
{
    Task<Money> ConvertAsync(Money source, string targetCurrency, CancellationToken ct);
}

Franchise Management

-- Add hierarchy
CREATE TABLE dbo.Stores (
  Id NVARCHAR(50) PRIMARY KEY,
  TenantId NVARCHAR(100) NOT NULL,
  Name NVARCHAR(200) NOT NULL,
  Address NVARCHAR(500),
  TimeZone NVARCHAR(50),
  IsActive BIT DEFAULT 1
);

CREATE TABLE dbo.Registers (
  Id NVARCHAR(50) PRIMARY KEY,
  StoreId NVARCHAR(50) NOT NULL REFERENCES dbo.Stores(Id),
  Name NVARCHAR(100),
  IsActive BIT DEFAULT 1
);

πŸ“ˆ Scalability Considerations

Current Architecture Scalability:

  • βœ… Horizontal scaling: Stateless API (can add more instances)
  • βœ… Database isolation: Each tenant DB scales independently
  • ⚠️ Master DB bottleneck: All requests query TenantMaster (mitigated by caching)

Future Enhancements:

  1. Read Replicas - Route read queries to replicas
  2. CQRS with Event Sourcing - Append-only event store
  3. Message Queue - Decouple commands (RabbitMQ/Azure Service Bus)
  4. API Gateway - Centralized routing, rate limiting, auth
  5. CDN for Static Assets - Receipt templates, product images

πŸŽ“ Learning Resources

Recommended Reading:

  • Designing Data-Intensive Applications (Martin Kleppmann) - Multi-tenancy patterns
  • Domain-Driven Design (Eric Evans) - Rich domain models
  • Building Microservices (Sam Newman) - Service boundaries

Patterns to Study:

  • Outbox Pattern - Reliable messaging
  • Saga Pattern - Distributed transactions
  • Circuit Breaker - Resilience
  • Strangler Fig - Legacy migration

πŸ§ͺ Testing & Load Testing

k6 Load Testing

The project includes comprehensive k6 test scripts for rate limiting and load testing:

Test Scripts:

  • k6-Tests/k6-simple-rate-limit.js - Simple rate limit test (10 VUs, 10s)
  • k6-Tests/k6-rate-limit-test.js - Advanced multi-tenant rate limit test with scenarios
  • k6-Tests/stampede-test.js - Cache stampede protection test

Run Tests:

# Start the API first (F5 in Visual Studio or dotnet run)
dotnet run --project MicroservicesBase.API

# Run stampede test (cache auto-warmed at startup - no manual pre-warming needed!)
k6 run k6-Tests/stampede-test.js

# Run rate limit test
k6 run k6-Tests/k6-rate-limit-test.js

Cache Warmup Strategy:

The API automatically pre-warms the tenant connection string cache at startup via CacheWarmupHostedService. This:

  • βœ… Eliminates cold-start delays - First request is fast (no 6.7s factory execution)
  • βœ… Prevents cache stampede - Under high concurrency, no requests block waiting for factory
  • βœ… Dynamic tenant discovery - Queries TenantMaster database for all active tenants (NO hardcoding!)
  • βœ… Production best practice - Follows Kubernetes init containers, health check patterns
  • βœ… Scalable - Works with 2 tenants or 2000 tenants automatically

Expected Startup Logs:

[12:34:56 INF] [] [] πŸ”₯ Starting cache warmup (dynamic tenant discovery)...
[12:34:56 INF] [] [] Found 2 active tenant(s) to warm: 7ELEVEN, BURGERKING
[12:34:59 INF] [] [] βœ… Pre-warmed cache for tenant: 7ELEVEN (1/2)
[12:35:02 INF] [] [] βœ… Pre-warmed cache for tenant: BURGERKING (2/2)
[12:35:02 INF] [] [] πŸ”₯ Cache warmup completed in 6234ms: 2 succeeded, 0 failed

Why This Matters:

Without warmup, the first request per tenant triggers:

  1. IMemoryCache miss β†’ Factory starts (thread-safe, blocks concurrent requests)
  2. MasterDb query via Polly pipeline (700ms timeout + 3 retries = 3-6 seconds)
  3. Under 200 req/s load, hundreds of requests pile up waiting
  4. Result: High latency, potential timeouts, poor user experience

With warmup:

  1. Cache pre-populated at startup (~3-6s one-time cost during deployment)
  2. All requests hit cache (<10ms)
  3. No blocking, no stampede, no latency spikes
  4. Production-ready! βœ…

Manual Pre-Warming (Optional):

If you disable the hosted service or want to warm specific tenants manually:

# Pre-warm specific tenants via API calls
curl -k -H "X-Tenant: 7ELEVEN" https://localhost:60304/api/v1/sales/11111111-1111-1111-1111-111111111111
curl -k -H "X-Tenant: BURGERKING" https://localhost:60304/api/v1/sales/11111111-1111-1111-1111-111111111111

Key Metrics Tracked:

  • Request success rate (200 vs 429 responses)
  • Latency percentiles (p50, p90, p95, p99)
  • Rate limit hit rate (per-tenant isolation)
  • Multi-tenant fairness (no cross-tenant interference)

Current Results (Post-Optimization):

Stampede Test (200 req/s, 10s):

  • βœ… 100% success rate (522/522 completed)
  • ⚠️ p95 latency: 3.26s (LocalDB bottleneck, SQL Server would be <50ms)
  • βœ… IMemoryCache working (connection strings cached, 10 min TTL)
  • βœ… Circuit breaker operational (Polly v8)
  • βœ… Multi-tenant isolation (7ELEVEN + BURGERKING)
  • ⚠️ 1594 dropped iterations (k6 VU limitation, not API fault)

Rate Limit Test (20 req/s, 35s):

  • βœ… 83% success rate under 2x overload
  • βœ… p95 latency: 318ms (target: <500ms)
  • βœ… p99 latency: ~1200ms (acceptable tail latency)
  • βœ… 17% rate limited (16.6% expected under 2x load)
  • βœ… Per-tenant rate limit isolation verified

Performance Notes:

  • LocalDB: 900ms queries under 200 req/s (development bottleneck, expected)
  • Production SQL Server: Expect <50ms p95 latency (10-20x faster)
  • IMemoryCache reduces TenantMaster load by 99%+ (only 2 queries for 522 requests)
  • SQL Connection Pool (200 max): Prevents connection exhaustion
  • Polly resilience: Auto-retry, circuit breaker, timeout enforcement working

Output Format: Beautiful boxed summary with:

  • πŸ“Š Request summary (total, success, failed, dropped with percentages)
  • ⚑ Latency distribution (all requests + success-only for cache analysis)
  • πŸ’Ύ Cache effectiveness estimation (based on p90/p50)
  • 🎯 POS performance verdict (automated pass/fail with βœ… ❌ indicators)
  • πŸ“ Tips & troubleshooting (actionable advice based on results)

🏁 Conclusion

This is an enterprise-grade multi-tenant POS system foundation with production-ready resilience, observability, and performance features. You've implemented:

βœ… 32 Enterprise Features including:

  • Polly v8 circuit breakers, retry policies, timeout enforcement
  • Structured logging with Serilog (correlation + tenant context)
  • Health checks, API versioning, rate limiting
  • Background audit logging with compression & PII masking
  • IMemoryCache connection string caching (99%+ TenantMaster load reduction)
  • Cache warmup on startup (IHostedService with dynamic tenant discovery)
  • Mapperly compile-time mappers (zero runtime overhead, type-safe)
  • Global exception handling with RFC 7807 ProblemDetails
  • Comprehensive k6 load testing with beautiful summaries

What's Left:

  1. Authentication/Authorization (JWT, RBAC, tenant claim validation) - 2-3 weeks
  2. Write Operations (CreateSale, VoidSale, RefundSale commands) - 2-3 weeks
  3. Idempotency Middleware (Idempotency-Key header handling) - 1 week
  4. Multi-Tenant Migrations (DbUp or FluentMigrator orchestration) - 1 week
  5. POS Domain Completion (payments, inventory, promotions) - 3-4 weeks

Estimated Timeline to Full Production:

  • Phase 1 (Auth): 2-3 weeks ⚠️ (your self-identified weak area)
  • Phase 2 (Write Operations): 2-3 weeks
  • Phase 3 (Domain Completion): 3-4 weeks
  • Total: 2-3 months

Current Production-Readiness:

  • βœ… Read Operations: Production-ready (with SQL Server, not LocalDB)
  • βœ… Resilience: Circuit breakers, retries, timeouts operational
  • βœ… Observability: Structured logging, health checks, request tracking
  • βœ… Performance: Optimized for high-concurrency POS workloads
  • ⚠️ Security: No auth yet (critical blocker for production)
  • ⚠️ Write Operations: Read-only system (major functionality gap)

The database-per-tenant strategy is perfect for POS systems where data isolation and compliance are paramount. Your use of FastEndpoints, Dapper, CQRS, and Polly v8 positions you well for high-performance, resilient transaction processing.


πŸ“ž Next Steps

  1. Review this analysis
  2. Prioritize features based on business needs
  3. Create a detailed implementation roadmap
  4. Set up development/staging environments
  5. Begin Phase 1 (Security & Stability)

Questions to Consider:

  • Which payment processors do you need to integrate?
  • What hardware (printers, scanners) will you support?
  • Do you need offline support?
  • What compliance requirements apply (PCI-DSS, GDPR, etc.)?
  • What reporting capabilities are essential?

Feel free to ask about any specific implementation details!

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published