Skip to content
bitzorcas
中EN

Concept

Billing Module Architecture: Subscriptions & Financial Guardrails

Deep dive into the BitzOrcas.Modern Billing Subsystem. Learn SubscriptionAggregate roots, atomic concurrency quota reservation, payment webhook idempotency, and invoice pipelines.

Last updated

In commercial SaaS platforms, Billing & Subscriptions is a mission-critical subsystem with zero tolerance for financial discrepancies:

  • Concurrency Quota Breaches: When multiple requests arrive concurrently demanding quota (e.g. AI tokens or storage), lack of atomic locking leads to negative balances and severe revenue leakage;
  • Webhook Retries & Out-of-Order Delivery: Payment gateways retry webhooks upon transient timeouts; handlers must enforce strict idempotency to prevent duplicate credit top-ups;
  • Subscription Lifecycle Finite State Machine: Managing transitions across Trial, Active, Dunning, Grace Period, and Suspended states.

BitzOrcas.Modern Billing Module builds an enterprise financial shield via “Atomic Mutexes + Strict State Machines + Idempotent Outbox Events”:

Subscription Lifecycle and Payment Fulfillment

Customer Pays

1. Tenant Client (Initiate Subscription / Upgrade)

2. CreateSubscriptionCommand (With Idempotency-Key)

3. Vertical Slice Handler

4. SubscriptionAggregate (Initialize Pending Subscription)

5. IPaymentGateway (Generate Hosted Checkout / QR)

6. PaymentWebhookController (Signature Verification + Deduplication)

7. FulfillSubscriptionCommand (Activate Subscription + Refresh Quota Pool)

8. CAP Domain Events (billing.subscription_activated)


Step 1: Subscription Aggregate Root and Lifecycle

SubscriptionAggregate.cs: Subscription Domain Aggregate Root
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Billing.Domain;
public static class SubscriptionErrors
{
public static readonly Error InvalidState =
Error.Conflict("Billing.InvalidState", "Subscription cannot be activated from current state.");
public static readonly Error NotFound =
Error.NotFound("Billing.SubscriptionNotFound", "Subscription not found.");
}
[BitzTable("BilSubscription", IsTenant = true, IsSoftDelete = true, Description = "Tenant Subscriptions Table")]
public sealed class SubscriptionAggregate : TenantAggregateRoot<string>
{
[BitzColumn(Length = 64, IsRequired = true)]
public string PlanCode { get; private set; } = string.Empty;
[BitzColumn(IsRequired = true)]
public SubscriptionStatus Status { get; private set; } = SubscriptionStatus.PendingPayment;
// Subscription time window
[BitzColumn(IsRequired = true)]
public DateTimeOffset StartsAt { get; private set; }
[BitzColumn(IsRequired = true)]
public DateTimeOffset ExpiresAt { get; private set; }
[Obsolete("For ORM materialization only. Use Create.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public SubscriptionAggregate()
: base("0")
{
}
// State machine transition activating subscription upon payment
public Result Activate(string transactionId, DateTimeOffset now)
{
if (Status == SubscriptionStatus.Active)
{
// Idempotency: already active returns success immediately
return Result.Success();
}
if (Status != SubscriptionStatus.PendingPayment)
{
return Result.Failure(SubscriptionErrors.InvalidState);
}
// State machine transition
Status = SubscriptionStatus.Active;
StartsAt = now;
ExpiresAt = now.AddMonths(1);
AddDomainEvent(new SubscriptionActivatedDomainEvent(Id, TenantId, PlanCode, ExpiresAt, transactionId));
return Result.Success();
}
}

Step 2: Idempotent Payment Webhook Fulfillment

ProcessPaymentWebhookCommandHandler.cs: Webhook Handler
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions.Idempotency;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
public sealed class ProcessPaymentWebhookCommandHandler(
ICommandRepository<SubscriptionAggregate, string> subscriptionRepo,
IIdempotencyStore idempotencyStore,
IAppClock clock)
{
public async ValueTask<Result> Handle(ProcessPaymentWebhookCommand command, CancellationToken ct)
{
// 1. Acquire distributed lock on external transaction ID
var lockKey = $"payment:webhook:{command.TransactionId}";
var acquired = await idempotencyStore.TryAcquireAsync(lockKey, ct);
if (!acquired.Acquired)
{
// Replay protection: return success on duplicate webhooks
return Result.Success();
}
// 2. Retrieve target subscription aggregate
var subscriptionResult = await subscriptionRepo.FindAsync(command.SubscriptionId, ct);
if (subscriptionResult.IsFailure)
{
return Result.Failure(SubscriptionErrors.NotFound);
}
var subscription = subscriptionResult.Value;
// 3. Execute domain state transition
var activateResult = subscription.Activate(command.TransactionId, clock.UtcNow);
if (activateResult.IsFailure) return activateResult;
// 4. Save aggregate (auto-committed by TransactionPipeline)
await subscriptionRepo.UpdateAsync(subscription, ct);
return Result.Success();
}
}

Summary

The Billing Module secures commercial revenue:

  • Zero Loss Prevention: Distributed mutexes + optimistic locking safeguard quota reservations;
  • End-to-End Idempotency: Payment webhooks and invoice generators are 100% idempotent;
  • Event-Driven Entitlements: Activated subscriptions immediately broadcast events, refreshing tenant feature quotas in milliseconds.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%