In legacy architectures, developers frequently call external email or SMS SDKs directly inside the main database transaction:
// Anti-pattern: Synchronous network call inside business transactionawait db.SaveChangesAsync();await emailSdk.SendAsync(...); // If external gateway times out for 10s, the caller hangs!This introduces fatal operational hazards:
- Network Latency Monopolizes DB Connections: External network delays hold database transaction locks;
- State Inconsistency: If email succeeds but the transaction rolls back, customers receive confirmation emails for aborted orders!
BitzOrcas.Modern uses “Notification Persistence First + CAP Transactional Outbox Asynchronous Fanout”: Business handlers simply persist in-app notification records, while asynchronous workers fan out delivery to external email, SMS, and enterprise IM connectors.
Multi-Channel Delivery Lifecycle
Step 1: Publishing Notification Facts
Business logic remains decoupled from delivery channels by persisting notifications:
using BitzOrcas.Domain.Results;
// The platform's single exit is NotificationService.ComposeAsync: one call writes// the inbox fact and feeds channel orchestration. There is no// INotificationAppService / CreateNotificationRequest.public sealed class LegalContractSignedHandler(NotificationService notifications){ public async ValueTask<Result> Handle(LegalContractSigned command, CancellationToken ct) { var created = await notifications.ComposeAsync( tenantId: command.TenantId, userId: command.SignerUserId, code: "contract.contract.signed", title: "Contract signed", body: $"Contract {command.ContractNo} has been signed; download your copy.", type: NotificationType.Business, category: command.BusinessType, severity: NotificationSeverity.Normal, linkUrl: null, linkText: null, metadataJson: metadataJson, cancellationToken: ct);
return created.IsSuccess ? Result.Success() : Result.Failure(created.Error); }}Step 2: Asynchronous Fanout and Preference Routing
Workers resolve channel preferences and deliver asynchronously:
public sealed class NotificationCreatedConsumer( INotificationOrchestrator orchestrator, IEmailDeliveryPort emailPort, ISmsDeliveryPort smsPort){ public async Task ProcessAsync(NotificationCreatedIntegrationEvent @event, CancellationToken ct) { // 1. Resolve user channel preferences var channels = await orchestrator.ResolveChannelsAsync( new NotificationContext(tenantId, userId, code, severity, category, metadataJson), ct);
// 2. Deliver in parallel (channel failures do not affect others) if (channels.Contains("Email")) { // EmailDeliveryRequest has six required parameters// (NotificationId/TenantId/RecipientUserId/Subject/TextBody/Channel); HtmlBody is optional.await emailPort.SendAsync(new EmailDeliveryRequest( To: @event.EmailAddress, Subject: @event.Title, HtmlBody: @event.Content), ct); } }}Summary
Decoupled messaging guarantees platform resilience:
- Zero Transaction Blocking: In-app notifications commit in milliseconds;
- Channel Fault Isolation: Email gateway outages never abort core operations;
- Extensible Adapters: Adding Slack or Telegram connectors requires zero domain changes.