CC Payment designs

I have read the documentation in depth.

I am a little confused and have a few questions so any help is appreciated. I have need to process 15 Credit Card authorizations per second. We use .NET and/or Azure. We have few connections but high volume messages (15 per second)

  1. Are Saga’s the only way to prevent duplicate Commands from being processed?

  2. I am unsure how to “scale out” to handle one connection but high volume of messages and ensure only one instance of the message is ever processed, Reliably. I can’t figure out if that means multiple instances or containers of RabbitMQ or NServiceBus listeners or what.

I am looking to implement low latency, reliable transactions that make calls to Visa, MC, etc…

If there are any basic examples explaining best practice - I’d greatly appreciate a link.

Hi

The design of such component greatly depends on the other components that interact with it, namely:

  • the upstream components that send the process payment commands
  • the downstream payment APIs of Visa, MC etc.

Let me explain these two sides. In order to process each transaction reliably once you need to ensure that each message is processed once. In systems that do not offer atomic send-and-publish through distributed transactions, such as when using RabbitMQ, you need to ensure that the upstream system that sends the process payment command uses consistent message ID generation strategy.

If that component is using NServiceBus then the Outbox feature will ensure this by storing messages in a database prior to sending.

Let’s now switch to the payment gateway side. These systems frequently allow the API client to provide a reference number. This value can be used in two way, depending on the API design. Some APIs if you pass the same reference number twice will automatically ignore the second request (deduplicate) and return the same status as for the first attempt. Other APIs utilise the query-before-post strategy and force you to first validate if the transaction is not a duplicate by querying an API and then actually calling the processing API. If you are dealing with a query-before-post system make sure to check the behaviour of the system if you don’t query and post the same reference number twice. A well-behaved system should return an error that points you to the query API. A badly designed one would happily process the transaction again. You really don’t want to deal with such a system because to interact with it you would need to use mutually-exclusive locks when calling the API. Such locks cannot be distributed which will impact your scalability options.

To summarise, depending on the payment API you can use one of the following designs. If the payment API automatically ignores duplicates and can accept NServiceBus message ID as a reference number then you don’t even need a saga. A simple handler that calls the payment API would be enough. The upstream component ensures duplicate messages have same IDs and these IDs are passed to the payment API as references. The payment API is going to handle the de-duplication. Here’s the pseudocode:

class ProcessPaymentHandler : IHandleMessages<ProcessPayment>
{
   public async Task Handle(ProcessPayment message, context)
   {
      var result = await paymentApi.Process(context.Headers[Headers.MessageID]);
      await context.Reply(new ProcessPaymentResponse(result));
   }
}

If you can’t use the message ID as a reference number (e.g. because the payment API has constraints on allowed characters etc) you need an additional step before the handler that calls the API. That step would be a saga that generates the reference numbers. Here’s some pseudocode:

class ReferenceNumberSaga : Saga<ReferenceNumberSagaData>,
   IAmStartedBy<ProcessPayment>,
   IHandleMessages<ProcessPaymentWithReferenceResponse>
{
   public async Task Handle(ProcessPayment message, context)
   {
      Data.ReferenceNumber = GenerateReferenceNumber();
      await context.Send(new ProcessPaymentWithReference(Data.ReferenceNumber));
   }

   public async Task Handle(ProcessPaymentWithReferenceResponse message, context)
   {
      await context.ReplyToOriginator(new ProcessPaymentResponse(message.Result));
   }
}

class ProcessPaymentWithReferenceHandler : IHandleMessages<ProcessPaymentWithReference>
{
   public async Task Handle(ProcessPaymentWithReference message, context)
   {
      var result = await paymentApi.Process(message.ReferenceNumber);
      await context.Reply(new ProcessPaymentWithReferenceResponse(result));
   }
}

When it comes to the throughput your main limiting factor is going to be the actual payment API. NServiceBus with RabbitMQ can easily cope with 1000s of messages per second. If the payment API invocation takes a lot of time you might want to tweak the concurrency level of your endpoint to allow for more concurrent work e.g. if you need to process 15 messages/s and each API invocation takes ~5 seconds then you need a total concurrency of 75 to achieve this. This means that at any given point in time you will have ~75 concurrent messages being handled.

If you are worried about downtime you can deploy two instances of such endpoint. These instances would share the load when both are up and running.

Hope it helps,
Szymon