Skip to content

🧭 Dhanman Messaging Design: Acknowledgment Event Pattern

Objective

Enable asynchronous, reliable communication between services using event-based acknowledgments instead of blocking request–response.
Each service that initiates a command will later receive a targeted acknowledgment event confirming completion and returning necessary identifiers.


🧩 1. Design Philosophy

  • Fire-and-Forget Commands: Sales, Purchase, Payroll, and Community send commands to Common or other shared modules.
  • Targeted Acknowledgment Events: The receiver (e.g., Common) publishes back an acknowledgment event containing IDs or status for the initiating entity.
  • Point-to-Point Routing: Each acknowledgment event is delivered only to its respective source service, avoiding fanout noise.
  • Guaranteed Delivery: All acknowledgment events use durable queues bound to a direct exchange with explicit routing keys.

🔁 2. High-Level Flow Example

sequenceDiagram
    participant Sales as Dhanman.Sales
    participant Common as Dhanman.Common

    Sales->>Common: 🔶 CreateTransactionCommand (InvoiceId, Amount)
    Common->>CommonDB: 💾 Create Transaction
    Common-->>Sales: 🔵 TransactionPostedForInvoiceEvent (InvoiceId, TransactionId)
    Sales->>SalesDB: 🧾 Update Invoice (TransactionId, Posted = true)

🧱 3. Exchange and Queue Strategy

Exchange Type Publisher Routing Key Pattern Consumer Service Queue Example
common.ack.events direct Common invoice.posted Sales sales.invoice_posted.queue
common.ack.events direct Common bill.posted Purchase purchase.bill_posted.queue
common.ack.events direct Common salary.posted Payroll payroll.salary_posted.queue
common.ack.events direct Common customer.created Sales / Community sales.customer_created.queue, community.customer_created.queue
common.ack.events direct Common vendor.created Purchase purchase.vendor_created.queue
common.ack.events direct Common employee.created Payroll payroll.employee_created.queue

🧩 4. Complete List of Acknowledgment Events

Origin Service (Sender) Target Publisher (Responder) Command Sent Acknowledgment Event Payload Fields (Suggested) Consumer Queue
Sales Common CreateBasicCustomerCommand CustomerCreatedForSalesEvent CustomerId, CompanyId, OrganizationId, CorrelationId, CreatedOnUtc sales.customer_created.queue
Common CreateTransactionCommand TransactionPostedForInvoiceEvent InvoiceId, TransactionId, CompanyId, OrganizationId, PostedOnUtc sales.invoice_posted.queue
Common CreateInvoicePaymentCommand TransactionPostedForInvoicePaymentEvent InvoicePaymentId, TransactionId, CompanyId, OrganizationId, PostedOnUtc sales.invoice_payment_posted.queue
Purchase Common CreateBasicVendorCommand VendorCreatedForPurchaseEvent VendorId, CompanyId, OrganizationId, CreatedOnUtc purchase.vendor_created.queue
Common CreateTransactionCommand TransactionPostedForBillEvent BillId, TransactionId, CompanyId, OrganizationId, PostedOnUtc purchase.bill_posted.queue
Common CreateBillPaymentCommand TransactionPostedForBillPaymentEvent BillPaymentId, TransactionId, CompanyId, OrganizationId, PostedOnUtc purchase.bill_payment_posted.queue
Payroll Common CreateBasicEmployeeCommand EmployeeCreatedForPayrollEvent EmployeeId, CompanyId, OrganizationId, CreatedOnUtc payroll.employee_created.queue
Common CreateTransactionCommand TransactionPostedForSalaryEvent SalaryId, TransactionId, CompanyId, OrganizationId, PostedOnUtc payroll.salary_posted.queue
Common CreateSalaryPaymentCommand TransactionPostedForSalaryPaymentEvent SalaryPaymentId, TransactionId, CompanyId, OrganizationId, PostedOnUtc payroll.salary_payment_posted.queue
Community Common CreateBasicCustomerCommand CustomerCreatedForCommunityEvent CustomerId, UnitId, CompanyId, OrganizationId, CreatedOnUtc community.customer_created.queue
Common (reverse ack) Community (Triggered when Unit creates a Customer) CustomerLinkedToUnitEvent UnitId, CustomerId, CompanyId, OrganizationId, LinkedOnUtc community.customer_linked.queue

⚙️ 5. MassTransit Configuration Example

Common.Api Publisher

await _eventPublisher.PublishAsync(new TransactionPostedForInvoiceEvent
{
    InvoiceId = command.InvoiceId,
    TransactionId = transaction.Id,
    CompanyId = command.CompanyId,
    OrganizationId = command.OrganizationId,
    PostedOnUtc = DateTime.UtcNow
}, context =>
{
    context.SetRoutingKey("invoice.posted");
});

Sales.Api Consumer

cfg.ReceiveEndpoint("sales.invoice_posted.queue", e =>
{
    e.ConfigureConsumeTopology = false;
    e.Bind("common.ack.events", x =>
    {
        x.RoutingKey = "invoice.posted";
    });
    e.ConfigureConsumer<TransactionPostedForInvoiceConsumer>(context);
});

🧠 6. Why Use Direct Exchange

✔ Only specific consumers receive relevant messages
✔ Reduces unnecessary message traffic
✔ Keeps event routing explicit and predictable
✔ Easy to extend — just add a new routing key and queue binding for another event type


🪶 7. Schema Example (Event DTO)

public record TransactionPostedForInvoiceEvent(
    Guid InvoiceId,
    long TransactionId,
    Guid CompanyId,
    Guid OrganizationId,
    DateTime PostedOnUtc,
    Guid CorrelationId);

All acknowledgment events follow a consistent convention:

  • Include CorrelationId from originating command
  • Include EntityId (InvoiceId, BillId, etc.)
  • Include TransactionId
  • Include OrganizationId, CompanyId, and Timestamp

🧩 8. Summary Checklist

✅ Goal 🧩 Implementation
Maintain async non-blocking flow Fire commands, listen for acknowledgment events
Event naming consistency Use TransactionPostedForXEvent, CustomerCreatedForXEvent, etc.
Routing isolation Use direct exchange with service-specific routing keys
Observability Include CorrelationId in every message
Reliability Use Outbox pattern for event publishing
Maintainability Group all acknowledgments under common.ack.events exchange