Understanding the modular monolith architecture in Wolfix.Server
Wolfix.Server implements a modular monolith architecture, which combines the organizational benefits of microservices with the simplicity of monolithic deployment.
// ❌ BAD: Direct dependencyusing Customer.Domain;public class OrderService{ private readonly CustomerRepository _customerRepo; // Direct coupling!}
// ✅ GOOD: Event-based communicationusing Shared.IntegrationEvents;public class OrderService{ private readonly EventBus _eventBus; public async Task CreateOrder() { // Request data via event var event = new GetCustomerDetails { CustomerId = id }; var result = await _eventBus.PublishWithSingleResultAsync<GetCustomerDetails, CustomerDto>(event, ct); }}
All inter-module communication goes through the EventBus:
Shared.IntegrationEvents/EventBus.cs
public class EventBus{ // Publish event and get a single result public async Task<Result<TResult>> PublishWithSingleResultAsync<TEvent, TResult>( TEvent @event, CancellationToken ct) { var handlers = _serviceProvider.GetServices<IIntegrationEventHandler<TEvent, TResult>>(); var handler = handlers.FirstOrDefault(); if (handler == null) return Result<TResult>.Failure("No handler found"); return await handler.HandleAsync(@event, ct); } // Publish event and get multiple results public async Task<IEnumerable<TResult>> PublishWithMultipleResultsAsync<TEvent, TResult>( TEvent @event, CancellationToken ct) { var handlers = _serviceProvider.GetServices<IIntegrationEventHandler<TEvent, TResult>>(); var tasks = handlers.Select(h => h.HandleAsync(@event, ct)); var results = await Task.WhenAll(tasks); return results.Where(r => r.IsSuccess).Select(r => r.Value!); }}
Use architecture tests to prevent direct dependencies:
[Fact]public void Modules_Should_Not_Reference_Other_Modules(){ var catalogAssembly = typeof(CatalogModule).Assembly; var forbiddenReferences = new[] { "Order", "Customer", "Seller" }; var references = catalogAssembly.GetReferencedAssemblies() .Select(a => a.Name); Assert.DoesNotContain(references, r => forbiddenReferences.Any(f => r.Contains(f)));}
2
Keep Events Focused
Events should represent business facts, not commands:
// ✅ GOOD: Business factpublic class OrderPlaced{ public Guid OrderId { get; init; } public Guid CustomerId { get; init; } public decimal TotalAmount { get; init; }}// ❌ BAD: Command disguised as eventpublic class CreateInvoiceForOrder { }
3
Handle Event Failures
Always handle event publishing failures:
var result = await eventBus.PublishWithSingleResultAsync<TEvent, TResult>(@event, ct);if (result.IsFailure){ // Log error, compensate, or return failure to user return Result<T>.Failure(result);}
4
Version Integration Events
Add version numbers to events for evolution:
public class CustomerCreatedV2{ public int Version => 2; public Guid CustomerId { get; init; } public string Email { get; init; } public DateTime CreatedAt { get; init; } // New field}