Skip to main content

System Architecture Overview

SpecKit Ticketing Platform is built on a distributed microservices architecture that combines Hexagonal Architecture (Ports & Adapters), CQRS, and Event-Driven Design to create a scalable, maintainable ticketing system.

Architecture Diagram

Core Architectural Principles

Each microservice is structured using hexagonal architecture, which isolates the domain logic from infrastructure concerns.Structure:
service/
├── Domain/           # Business logic & entities
│   ├── Entities/     # Domain models
│   └── Ports/        # Interfaces (abstractions)
├── Application/      # Use cases & orchestration
│   ├── UseCases/     # Command/Query handlers
│   └── Ports/        # Application-level interfaces
├── Infrastructure/   # External adapters
│   ├── Persistence/  # Database implementation
│   ├── Messaging/    # Kafka producers/consumers
│   └── Locking/      # Redis lock implementation
└── Api/              # HTTP endpoints
Key Benefits:
  • Domain logic remains pure and testable
  • Easy to swap infrastructure implementations
  • Clear separation of concerns
  • Dependency inversion: Domain depends on abstractions, not implementations
The system is divided into distinct bounded contexts, each owning its data and business logic:
Bounded ContextResponsibilityKey Entities
CatalogEvent browsing, seat informationEvent, Seat
InventorySeat reservations, availabilityReservation, Seat
OrderingShopping cart, order managementOrder, OrderItem
PaymentPayment processing, validationPayment, Transaction
FulfillmentTicket generation, deliveryTicket
IdentityUser authentication, authorizationUser, Token
NotificationEmail notificationsEmailNotification
Context Independence:
  • Each context has its own database schema (e.g., bc_catalog, bc_inventory)
  • Services communicate via well-defined contracts
  • No direct database sharing between contexts
Each microservice follows a consistent layering strategy:Layer Responsibilities:
Role: HTTP endpoints and request/response handling
// services/ordering/src/Api/Controllers/CartController.cs
[ApiController]
[Route("api/[controller]")]
public class CartController : ControllerBase
{
    private readonly IMediator _mediator;

    [HttpPost("add")]
    public async Task<IActionResult> AddToCart(
        [FromBody] AddToCartRequest request)
    {
        var command = new AddToCartCommand(
            request.SeatId, 
            request.UserId, 
            request.GuestToken
        );
        
        var result = await _mediator.Send(command);
        return Ok(result);
    }
}

Communication Patterns

Synchronous (REST)

When to Use:
  • Immediate queries (browsing events)
  • Real-time validation (seat availability)
  • User-initiated actions requiring instant feedback
Examples:
  • GET /api/events → Catalog Service
  • POST /api/reservations → Inventory Service
  • POST /api/cart/add → Ordering Service

Asynchronous (Kafka)

When to Use:
  • Long-running workflows (payment processing)
  • Event choreography across services
  • Eventual consistency scenarios
Examples:
  • reservation-created event triggers order creation
  • payment-succeeded event triggers ticket fulfillment
  • ticket-issued event triggers notification

Data Consistency Strategy

Used for: Critical operations within a bounded contextImplementation:
  • PostgreSQL transactions with ACID guarantees
  • Unit of Work pattern via EF Core’s SaveChangesAsync()
  • Redis distributed locks for cross-instance coordination
Example:
// Atomic seat reservation with distributed lock
var lockToken = await _redisLock.AcquireLockAsync(lockKey, ttl);
try
{
    seat.Reserved = true;
    _context.Reservations.Add(reservation);
    await _context.SaveChangesAsync(); // Atomic transaction
}
finally
{
    await _redisLock.ReleaseLockAsync(lockKey, lockToken);
}

Technology Stack

ComponentTechnologyPurpose
Backend.NET 9, Minimal APIsHigh-performance microservices
MessagingMediatRIn-process command/query handling
ORMEntity Framework CoreDatabase access with migrations
Event BusApache KafkaAsynchronous event choreography
Caching/LockingRedisDistributed locks, session state
DatabasePostgreSQLPersistent data storage (schema-per-service)
ObservabilityOpenTelemetry, SerilogDistributed tracing and logging
FrontendNext.js 14, TailwindCSSModern React-based UI

Key Design Decisions

Benefits Realized:
  1. Testability: Domain logic can be tested without infrastructure
    // Pure unit test without database or Kafka
    var mockRedisLock = new MockRedisLock(acquireResult: "token-123");
    var mockKafka = new MockKafkaProducer();
    var handler = new CreateReservationCommandHandler(
        context, mockRedisLock, mockKafka
    );
    
  2. Flexibility: Easy to swap implementations
    • Production: RedisLock → StackExchange.Redis
    • Testing: MockRedisLock → In-memory simulation
  3. Clear Boundaries: Domain never references infrastructure types
    // Domain port (interface)
    public interface IRedisLock
    {
        Task<string?> AcquireLockAsync(string key, TimeSpan ttl);
    }
    
    // Infrastructure adapter
    public class RedisLock : IRedisLock { /* Redis-specific code */ }
    
Rationale:
  • Bounded Context Isolation: Each service owns its schema (bc_inventory, bc_ordering)
  • Independent Deployment: Schema migrations don’t affect other services
  • Data Integrity: No cross-schema foreign keys prevent accidental coupling
  • Future-Proof: Easy to split into separate databases if needed
-- Each service creates its own schema
CREATE SCHEMA IF NOT EXISTS bc_inventory;
CREATE SCHEMA IF NOT EXISTS bc_ordering;
CREATE SCHEMA IF NOT EXISTS bc_catalog;
Synchronous REST is used when:
  • User expects immediate feedback (seat availability check)
  • Operation is simple and fast (query events)
  • Strong consistency is required within a single context
Asynchronous Kafka is used when:
  • Operation involves multiple services (checkout → payment → fulfillment)
  • Process is long-running (payment gateway integration)
  • Services should remain decoupled (notification doesn’t block fulfillment)
Example Workflow:
1. User reserves seat (Sync REST)  → Immediate response
2. Inventory publishes event (Async) → Ordering service reacts
3. User checks out (Sync REST)     → Order created
4. Payment processed (Async)       → Payment service handles
5. Ticket issued (Async)           → Fulfillment service generates
6. Email sent (Async)              → Notification service delivers

Microservices Design

Learn about service boundaries, independence, and deployment strategies

Event-Driven Architecture

Deep dive into Kafka events, async workflows, and choreography

CQRS Pattern

Understand command/query separation with MediatR

Hexagonal Architecture

Detailed exploration of ports, adapters, and dependency inversion

Build docs developers (and LLMs) love