Architecture Overview
System Architecture
The Book Store API is built using Event Sourcing and CQRS patterns with ASP.NET Core Minimal APIs, Marten for event storage, and PostgreSQL as the database.
┌─────────────────────────────────────────────────────────────┐
│ Client Applications │
│ (Web UI, Mobile Apps, Third-party Integrations) │
└────────────────────────┬────────────────────────────────────┘
│
│ HTTP/REST
│
┌────────────────────────▼────────────────────────────────────┐
│ Book Store API │
│ (ASP.NET Core Minimal APIs) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Public │ │ Admin │ │ System │ │
│ │ Endpoints │ │ Endpoints │ │ Endpoints │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ │ │ │
│ ┌──────▼──────────────────▼──────────────────▼───────┐ │
│ │ Command Handlers (Aggregates) │ │
│ │ ┌──────┐ ┌────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │ Book │ │ Author │ │ Category │ │Publisher│ │ │
│ │ └──────┘ └────────┘ └──────────┘ └─────────┘ │ │
│ └─────────────────────┬──────────────────────────────┘ │
│ │ │
│ │ Events │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Marten Event Store │ │
│ │ - Append events │ │
│ │ - Stream management │ │
│ │ - Correlation/Causation tracking │ │
│ └────────────────────┬───────────────────────────────┘ │
│ │ │
│ │ Async Projections │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Read Models (Projections) │ │
│ │ ┌──────────────┐ ┌────────────┐ ┌──────────┐ │ │
│ │ │BookSearch │ │ Author │ │ Category │ │ │
│ │ │Projection │ │ Projection │ │Projection│ │ │
│ │ └──────────────┘ └────────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────────┘
│
│ PostgreSQL Protocol
▼
┌─────────────────────────────────────────────────────────────┐
│ PostgreSQL Database │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ mt_events │ │ mt_streams │ │ Projections │ │
│ │ (Events) │ │ (Metadata) │ │ (Read Models) │ │
│ └──────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Core Patterns
Event Sourcing
Instead of storing current state, we store all changes as immutable events.
Note
For a comprehensive guide to event sourcing concepts, patterns, and best practices, see the Event Sourcing Guide.
Benefits:
- Complete audit trail
- Time travel (reconstruct past states)
- Event replay for debugging
- Natural fit for distributed systems
Implementation:
// Events are immutable records
public record BookAdded(
Guid Id,
string Title,
string? Isbn,
...
);
// Aggregates apply events to build state
public class BookAggregate
{
public void Apply(BookAdded @event)
{
Id = @event.Id;
Title = @event.Title;
...
}
}
See Marten Guide for implementation details.
CQRS (Command Query Responsibility Segregation)
Separate models for writes (commands) and reads (queries).
Write Side (Commands):
- Commands routed through Wolverine message bus
- Handlers execute business logic
- Events are appended to streams
- Optimized for consistency
Read Side (Queries):
- Projections denormalize data
- Optimized for specific queries
- Eventually consistent
Wolverine Command/Handler Pattern
Commands are routed through Wolverine's message bus to handlers that execute business logic.
Command Flow:
HTTP Request → Endpoint → Command → IMessageBus → Handler → Aggregate → Event → Auto-commit
Benefits:
- Clean separation of concerns
- Automatic transaction management
- Easy to test (pure functions)
- Foundation for async messaging
Example:
// Endpoint: Just routing
private static Task<IResult> CreateBook(request, IMessageBus bus)
=> bus.InvokeAsync<IResult>(new CreateBook(...));
// Handler: Pure business logic
public static IResult Handle(CreateBook cmd, IDocumentSession session)
{
var @event = BookAggregate.Create(...);
session.Events.StartStream(cmd.Id, @event);
// Wolverine auto-commits
return Results.Created(...);
}
Async Projections
Projections run asynchronously to build read models from events. With Wolverine integration via .IntegrateWithWolverine(), Wolverine manages the async projection daemon, providing distributed coordination and automatic failover across nodes.
public class BookSearchProjection
{
// Denormalized for fast searching
public string Title { get; set; }
public string? PublisherName { get; set; }
public string AuthorNames { get; set; }
public string SearchText { get; set; }
}
Domain Model
Aggregates
Book Aggregate:
- Root entity for book management
- Enforces business rules
- Emits events:
BookAdded,BookUpdated,BookSoftDeleted,BookRestored
Author Aggregate:
- Manages author information
- Tracks biography and metadata
Category Aggregate:
- Supports multi-language translations
- Manages category hierarchy
Publisher Aggregate:
- Publisher information management
Events
All events include:
- Domain data (title, ISBN, etc.)
- Marten metadata (correlation ID, causation ID, timestamp)
Example event flow:
1. User creates book → BookAdded event
2. Event stored in mt_events table
3. Async projection updates BookSearchProjection
4. Read model available for queries
Data Flow
Write Path (Command)
1. HTTP Request → Endpoint
2. Load Aggregate from Event Stream
3. Execute Business Logic
4. Generate Domain Event
5. Append Event to Stream
6. SaveChanges (atomic)
7. Return Response
Read Path (Query)
1. HTTP Request → Endpoint
2. Query Projection (Read Model)
3. Apply Filters/Pagination
4. Return Results
Projection Update (Async)
1. Event Appended to Stream
2. Async Daemon Detects New Event
3. Projection Builder Processes Event
4. Update Read Model
5. Commit Changes
Technology Stack
Backend
- ASP.NET Core 10 - Web framework
- Minimal APIs - Endpoint definition
- Wolverine 5.9.2 - Command/handler pattern and message bus
- Marten 8.17.0 - Event store and document DB
- PostgreSQL 16 - Database
- Aspire - Orchestration and observability
- Scalar 2.11.10 - API documentation
Features
- Event Sourcing - Marten event store
- CQRS - Separate read/write models
- Optimistic Concurrency - ETags with stream versions
- Distributed Tracing - Correlation/causation IDs
- Multi-language - Category translations
- Full-text Search - PostgreSQL trigrams
- API Versioning - Header-based
- Soft Deletion - Logical deletes with restore
Infrastructure
- Docker - Container runtime
- PgAdmin - Database management
- OpenTelemetry - Distributed tracing
- Health Checks - Service monitoring
Key Design Decisions
1. Event Sourcing with Marten
Why:
- Built-in event store on PostgreSQL
- No additional infrastructure needed
- Strong .NET integration
- Async projection support
Trade-offs:
- Learning curve for event sourcing
- Eventually consistent reads
- More complex than CRUD
2. Minimal APIs
Why:
- Less boilerplate than controllers
- Better performance
- Cleaner endpoint definition
- Native OpenAPI support
3. Async Projections
Why:
- Decouples write and read models
- Optimized read models for specific queries
- Scalable (can run on separate processes)
Trade-offs:
- Eventually consistent
- Projection lag possible
- More complex than direct queries
4. Soft Deletion
Why:
- Preserve data integrity
- Support undo/restore
- Maintain referential integrity
- Audit trail
5. ETags for Concurrency
Why:
- Standard HTTP mechanism
- Works with any client
- Natural fit with stream versions
- Prevents lost updates
Scalability Considerations
Horizontal Scaling
- API Servers: Stateless, can scale horizontally
- Projection Daemon: Can run on dedicated instances
- PostgreSQL: Read replicas for queries
Performance Optimizations
- Output Caching: Public endpoints cached
- Connection Pooling: Npgsql connection pooling
- Async Projections: Non-blocking event processing
- Denormalization: Optimized read models
Event Store Growth
- Archiving: Old streams can be archived
- Snapshots: Aggregate snapshots for large streams (future)
- Partitioning: PostgreSQL table partitioning (future)
Security Considerations
Authentication & Authorization
- Future: Add JWT authentication
- Future: Role-based authorization
- Current: Admin endpoints unprotected (development only)
Data Protection
- Soft Deletion: Prevents accidental data loss
- Event Immutability: Events cannot be modified
- Audit Trail: Complete history of all changes
Monitoring & Observability
Health Checks
- PostgreSQL connectivity
- Marten event store
- Projection daemon status
Distributed Tracing
- Correlation IDs track workflows
- Causation IDs track event chains
- OpenTelemetry integration
Logging
- Structured logging with Serilog
- Aspire dashboard for log aggregation
- Event store for audit logs
Next Steps
- Event Sourcing Guide - Event sourcing concepts and patterns
- Marten Guide - Event sourcing implementation with Marten
- Wolverine Guide - Command/handler pattern
- Aspire Orchestration Guide - Service orchestration details
- Getting Started - Setup and running the application