Architecture Overview
The system consists of five core services orchestrated through .NET Aspire:Service Orchestration with .NET Aspire
The application host (AppHost.cs) defines the entire distributed system topology:
Core Architecture Principles
1. Database Per Service
Each service maintains its own dedicated PostgreSQL database:Database Isolation Example
Database Isolation Example
- Data autonomy: Each service owns its data schema
- Independent scaling: Services can scale their databases independently
- Fault isolation: Database issues don’t cascade across services
- Technology flexibility: Services can choose different storage technologies
2. API Gateway Pattern
The Gateway API uses YARP (Yet Another Reverse Proxy) to route requests to backend services:3. Service Discovery
Services discover each other using .NET Aspire’s built-in service discovery:src/services/Users/Users.Api/Program.cs
The
https+http:// scheme allows the client to prefer HTTPS but fall back to HTTP during development.4. Asynchronous Communication
Services communicate asynchronously through RabbitMQ using the Wolverine messaging framework:src/services/Users/Users.Api/Program.cs
5. Observability
All services export telemetry to OpenTelemetry collectors:Service Telemetry Configuration
- Prometheus: Metrics collection
- Jaeger: Distributed tracing
- Loki: Log aggregation
- Grafana: Unified observability dashboard
Service Boundaries
Domain-Driven Design
Each service represents a bounded context:Identity Service
Authentication, authorization, token issuance
Users Service
Driver, passenger, admin, vehicle, wallet management
Trips Service
Trip planning, booking, seat management, payment processing
Notifications Service
Push notifications, device tokens, notification history
Cross-Service Communication Patterns
Synchronous HTTP Calls
Synchronous HTTP Calls
Used for read operations where immediate consistency is required:
Asynchronous Events
Asynchronous Events
Used for write operations and notifications:Benefits:
- Decoupling: Services don’t need to know about consumers
- Reliability: Messages are persisted until processed
- Scalability: Can add new consumers without modifying publishers
Transactional Outbox Pattern
Transactional Outbox Pattern
Ensures exactly-once delivery by persisting messages to PostgreSQL:Workflow:
- Message is written to
wolverine.outboxtable in same transaction as business data - Background worker polls outbox and publishes to RabbitMQ
- Message is marked as sent only after RabbitMQ confirms
Deployment Dependencies
Services have explicit startup dependencies defined in AppHost:Service Dependency Chain
The
WaitFor() method ensures services start in the correct order, preventing startup failures from missing dependencies.Benefits of This Architecture
Independent Scaling
Scale high-traffic services (e.g., Trips) without affecting others
Technology Diversity
Use different frameworks, languages, or databases per service
Fault Isolation
Failures in one service don’t cascade to others
Team Autonomy
Teams can develop, test, and deploy services independently
Easier Maintenance
Smaller codebases are easier to understand and modify
Flexible Deployment
Deploy services independently with different release cycles
Trade-offs and Challenges
Related Topics
Services Overview
Detailed breakdown of each service’s responsibilities
Messaging
RabbitMQ and Wolverine event-driven patterns
Database Schema
PostgreSQL schema and migrations per service
Authentication
OpenID Connect and JWT token flow