Skip to main content
Masar Eagle follows a microservices architecture pattern where the application is decomposed into loosely coupled, independently deployable services. Each service owns its domain logic, data, and communicates with others through well-defined interfaces.

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:
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);

// Infrastructure resources
IResourceBuilder<PostgresServerResource> postgres = builder.AddPostgres(Components.Postgres)
    .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "trust")
    .WithDataVolume(name: "masar-postgres-data", isReadOnly: false)
    .WithPgAdmin(pgAdmin => pgAdmin.WithHostPort(5050));

IResourceBuilder<RabbitMQServerResource> rabbitmq = builder.AddRabbitMQ(Components.RabbitMQ, username, password)
    .WithDataVolume(Components.RabbitMQConfig.DataVolumeName)
    .WithManagementPlugin(port: int.Parse(Components.RabbitMQConfig.ManagementPort));

// Core services
IResourceBuilder<ProjectResource> identityApi = builder.AddProject<Identity>(Services.Identity)
    .WithReference(authDb)
    .WithReference(rabbitmq)
    .WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", otelEndpoint)
    .WithExternalHttpEndpoints()
    .WaitFor(authDb)
    .WaitFor(rabbitmq);

IResourceBuilder<ProjectResource> usersApi = builder.AddProject<User>(Services.User)
    .WithReference(usersDb)
    .WithReference(rabbitmq)
    .WithReference(identityApi)  // For JWT validation
    .WaitFor(usersDb)
    .WaitFor(rabbitmq);

Core Architecture Principles

1. Database Per Service

Each service maintains its own dedicated PostgreSQL database:
// Users service has its own database
IResourceBuilder<PostgresDatabaseResource> usersDb = postgres.AddDatabase(Components.Database.User);

// Trips service has its own database
IResourceBuilder<PostgresDatabaseResource> tripsDb = postgres.AddDatabase(Components.Database.Trip);

// Notifications service has its own database
IResourceBuilder<PostgresDatabaseResource> notificationsDb = postgres.AddDatabase(Components.Database.Notifications);

// Identity service has its own database
IResourceBuilder<PostgresDatabaseResource> authDb = postgres.AddDatabase(Components.Database.Auth);
This ensures:
  • 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:
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddServiceDiscoveryDestinationResolver()
    .AddTransforms(context => context.AddXForwarded(ForwardedTransformActions.Set));

app.MapReverseProxy();

3. Service Discovery

Services discover each other using .NET Aspire’s built-in service discovery:
src/services/Users/Users.Api/Program.cs
builder.Services.AddHttpClient<TripsUsageClient>("trip", 
    client => client.BaseAddress = new Uri("https+http://trip"))
    .AddServiceDiscovery();  // Automatic service resolution

builder.Services.AddHttpClient<NotificationsClient>("notifications", 
    client => client.BaseAddress = new Uri("https+http://notifications"))
    .AddServiceDiscovery();
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
await builder.UseWolverineWithRabbitMqAsync(
    new WolverineMessagingOptions
    {
        EnablePostgresOutbox = true,  // Transactional outbox pattern
        PostgresConnectionName = Components.Database.User,
        OutboxSchema = "wolverine"
    },
    opts =>
    {
        opts.PublishAllMessages().ToRabbitExchange(Components.RabbitMQConfig.ExchangeName);
        
        opts.ListenToRabbitQueue("users-api-queue",
            cfg => cfg.BindExchange(Components.RabbitMQConfig.ExchangeName));
        
        opts.ApplicationAssembly = typeof(Program).Assembly;
    });

5. Observability

All services export telemetry to OpenTelemetry collectors:
Service Telemetry Configuration
builder.AddServiceDefaults();  // Adds health checks, telemetry, service discovery

.WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otelcollector:4317")
.WithEnvironment("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
.WithEnvironment("OTEL_SERVICE_NAME", "user")
The stack includes:
  • 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

Used for read operations where immediate consistency is required:
// Users service calling Trips service
public interface ITripsUsageClient
{
    Task<TripUsageResult> GetDriverTripUsageAsync(string driverId);
}
Avoid deep call chains that create tight coupling between services.
Used for write operations and notifications:
// Publishing an event
await messageBus.PublishAsync(
    new UserAuthenticatedEvent(userId, userType, phoneNumber));

// Handling an event in another service
public class UserAuthenticatedHandler
{
    public async Task Handle(UserAuthenticatedEvent @event)
    {
        // Provision user in local database
    }
}
Benefits:
  • Decoupling: Services don’t need to know about consumers
  • Reliability: Messages are persisted until processed
  • Scalability: Can add new consumers without modifying publishers
Ensures exactly-once delivery by persisting messages to PostgreSQL:
opts.PersistMessagesWithPostgresql(connectionString, "wolverine");
Workflow:
  1. Message is written to wolverine.outbox table in same transaction as business data
  2. Background worker polls outbox and publishes to RabbitMQ
  3. Message is marked as sent only after RabbitMQ confirms
This prevents message loss even if RabbitMQ is temporarily unavailable.

Deployment Dependencies

Services have explicit startup dependencies defined in AppHost:
Service Dependency Chain
gatewayApi
    .WaitFor(usersApi)
    .WaitFor(tripsApi)
    .WaitFor(notificationsApi)
    .WaitFor(identityApi);

usersApi
    .WaitFor(usersDb)
    .WaitFor(rabbitmq)
    .WaitFor(identityApi);  // JWT key discovery

tripsApi
    .WaitFor(tripsDb)
    .WaitFor(rabbitmq)
    .WaitFor(usersApi);  // User validation
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

Increased Complexity: Distributed systems require:
  • Service discovery and health monitoring
  • Distributed tracing and logging
  • Network reliability and retries
  • Data consistency across services
  • Integration testing complexity

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

Build docs developers (and LLMs) love