Using Heroku To Leverage CloudAMQP

Written by johnjvester | Published 2021/06/13
Tech Story Tags: heroku | amqp | twilio | spring-boot | spring-framework | java | rabbitmq | tutorial

TLDR Heroku offers CloudAMQP messaging solutions within the Heroku ecosystem. The invoice process is designed to be quick and easy to use and accurate. Using RabbitMQ, the trainer can submit an invoice to clients via SMS, including a link to an online invoice. Using the invoice, the end-user can use the first link to view an online version of their invoice: The customer can click the Pay Invoice button to pay for their sessions using Venmo or click the second link on the original SMS message:via the TL;DR App

This article is going to focus on invoicing clients for services that have been performed and will utilize messaging solutions within the Heroku ecosystem. The goals of the invoice process are as follows:
  1. Quick and easy to use
  2. Accurate and allow adjustments
  3. Notify clients via SMS, including a link to an online invoice
  4. Allow clients to submit payments using Venmo
Below, is a current version of the feature roadmap:

Invoice Processing and Flow

In order to illustrate the flow for sending invoices, the following diagram was created:
I wanted to utilize a message-based approach, based upon my past experiences and satisfaction with the pattern. While one could argue that the current processing volume does not warrant a message-based approach, I did want to prove out this concept for use when the system gains more popularity.

About CloudAMQP

RabbitMQ is an open-source message broker solution, which allows asynchronous communication between different components of a multi-tier application. CloudAMQP is RabbitMQ wrapped into a service offering. Heroku not only offers CloudAMQP at price levels that match working directly with CloudAMQP, but also makes CloudAMQP very easy to install into an application. My experience with RabbitMQ and the ease of getting the message broker up and running made this product solution quite easy for me.
In a message broker solution, there are three main elements:
  • Producer that has a message to be processed
  • Queue that is the mechanism to store messages until they are processed
  • Consumer that watches the queue and handles the necessary processing
The producer requesting a task to be completed places a message on what is called a queue. In this case, the Invoice Submission process is handed off to an asynchronous process - which avoids making the Trainer wait for the invoicing process to complete. As a result, once the Trainer submits the request, the Fitness application can navigate to other sections of the application and continue working.
On the services side, the CloudAMQP service now has a message to be processed. A consumer (running in the Fitness application service within Heroku) listens for any messages to arrive and pulls them off the queue for processing, In the case of the Fitness application, the messages will be processed and sent to the customers via the Twilio service.
Invoicing Example
At a high level, the following actions take place during invoice processing:
  1. Trainer navigates to the Create Invoices menu option to request a list of current invoices.
  2. The Spring Boot service performs the necessary actions to return a List<InvoiceDto> of invoice data.
  3. The trainer can make adjustments to the invoice list, including changing a session to no-cost.
  4. When ready, the trainer uses the Submit Invoices button to initiate the invoice process.
  5. The Spring Boot service persists the InvoiceDTO items.
  6. Each InvoiceDTO is sent to CloudAMQP and added to the queue for processing asynchronously.
  7. An instance of the Spring Boot service picks up the job from the queue, processes the request, which sends an SMS message to the client for the invoice.
At this point, each InvoiceDto is available for the trainer to view.

The Customer Flow

The customer receives an SMS message similar to what is displayed below:
At that point, the end-user can use the first link to view an online version of their invoice:
Using the invoice, the customer can click the Pay Invoice using Venmo button to pay for their sessions using Venmo or they can click the second link on the original SMS message. Both messages navigate the user to the following page:
Here the user can sign-in or sign-up for Venmo. Once authenticated, the user can accept the request for payment and pay using the Venmo mobile application.

Configuring CloudAMQP in Heroku with Spring Boot

Getting started with CloudAMQP in Heroku is as easy as running the following command from the Heroku CLI:
heroku addons:create cloudamqp:lemur
Heroku adds everything your app needs to start working with the queue. Now we need to configure our app to use the queue, and define the message that we'll be passing through the queue.
The properties related to CloudAMQP will now be available in the Heroku instance:
These configuration values are ultimately set in Heroku as shown below:
Within Spring Boot, the pom.xml needs to be updated to include the following dependency to support Rabbit AMQP:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
Within the application.yml the following properties are introduced in order to allow the values to be dynamic:
jvc:
messaging:
invoice-topic-exchange: invoice-topic-exchange
invoice-processing-queue: invoice-processing-queue
 invoice-routing-key: invoice-processing.#
The three custom properties are available in Spring Boot as part of the MessagingConfigurationProperties bean:
@Data
@Configuration("messagingConfigurationProperties")
@ConfigurationProperties("jvc.messaging")
public class MessagingConfigurationProperties {
  private String invoiceTopicExchange;
  private String invoiceProcessingQueue;
  private String invoiceRoutingKey;
}
Finally, the messaging configuration class is configured as shown below:
@RequiredArgsConstructor
@Configuration
@EnableRabbit
public class MessagingConfig {
   private static final int MAX_CONSUMERS = 2;

   private final ConnectionFactory connectionFactory;
   private final MessagingConfigurationProperties messagingConfigurationProperties;

   @Bean
   public AmqpAdmin amqpAdmin() {
       return new RabbitAdmin(connectionFactory);
   }

   @Bean(name = "invoiceProcessingRabbitListenerContainerFactory")
   SimpleRabbitListenerContainerFactory invoiceProcessingRabbitListenerContainerFactory() {
       SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
       factory.setConnectionFactory(connectionFactory);
       factory.setMaxConcurrentConsumers(MAX_CONSUMERS);
       return factory;
   }

   @Bean(name = "invoiceProcessingQueue")
   Queue invoiceProcessingQueue() {
       return new Queue(messagingConfigurationProperties.getInvoiceProcessingQueue(), false);
   }

   @Bean(name = "invoiceTopicExchange")
   TopicExchange invoiceTopicExchange() {
       return new TopicExchange(messagingConfigurationProperties.getInvoiceTopicExchange());
   }

   @Bean
   Binding invoiceBinding(@Qualifier("invoiceProcessingQueue") Queue queue, @Qualifier("invoiceTopicExchange") TopicExchange exchange) {
       return BindingBuilder.bind(queue).to(exchange)
       .with(messagingConfigurationProperties.getInvoiceRoutingKey());
   }
}
At this point, Spring Boot can now connect to CloudAMQP to process messages in the topic exchange and queue.

Processing Requests

To process invoices using the CloudAMQP service, a request enters the system via the InvoiceService (the producer):
private void sendMessages(List<Invoice> invoices, boolean resend) throws FitnessException {
   if (CollectionUtils.isNotEmpty(invoices)) {
       for (Invoice invoice : invoices) {
             handleInvoice(invoice);
             invoicePublisher.sendMessage(new InvoiceProcessingRequest(invoice.getId(),
               userData.getTenant().getTenantProperties().getTimeZone(),
               userData.getTenant().getTenantProperties().getVenmoId(), resend));
       }
   }
}
The InvoicePublisher class performs the task of sending the message to CloudAMQP (the queue):
@TransactionalEventListener(fallbackExecution = true)
public void sendMessage(InvoiceProcessingRequest invoiceProcessingRequest) throws FitnessException {
   try {
       if (invoiceProcessingRequest != null) {
           rabbitTemplate.convertAndSend(messagingConfigurationProperties.getInvoiceTopicExchange(),
             messagingConfigurationProperties.getInvoiceRoutingKey(),
             objectMapper.writeValueAsString(invoiceProcessingRequest));
       }
   } catch (JsonProcessingException| AmqpException e) {
       throw new FitnessException(FitnessException.UNKNOWN_ERROR, "Error attempting to process invoice: " + e.getMessage());
   }
}
From there the InvoiceProcessor class (the consumer,running on a Spring Boot service API instance), receives the message and processes the request:
@RabbitListener(containerFactory="invoiceProcessingRabbitListenerContainerFactory",
           queues="#{messagingConfigurationProperties.invoiceProcessingQueue}")
@Transactional
public void receiveMessage(String message) {
   try {
       InvoiceProcessingRequest invoiceProcessingRequest = objectMapper.readValue(message, InvoiceProcessingRequest.class);

       Optional<Invoice> optional = invoiceRepository.findById(invoiceProcessingRequest.getInvoiceId());

       if (optional.isPresent()) {
           smsService.sendSmsInvoice(optional.get(),
             invoiceProcessingRequest.getTimeZone(),
             invoiceProcessingRequest.getVenmoId(),
             invoiceProcessingRequest.isResend());
           invoiceRepository.save(optional.get());
       } else {
           log.error("Could not locate invoiceId={}", invoiceProcessingRequest.getInvoiceId());
       }

   } catch (Exception e) {
       log.error("An error occurred attempting to process message={}", message, e);
   }
}
The SmsService sends the invoice information over SMS and the Invoice object is marked as processed.

Conclusion

Before this functionality was introduced, my sister-in-law was manually sending invoices to her clients. She would look back at the sessions that had been completed since their last invoice, then note the cost for each session and the date range. The next step would be to paste a generic message about the invoice and update the values to match her client's information.
The new process takes a mere fraction of the time to complete and has the ability to resend invoices at a later time. The system is smart enough to know which clients to invoice and which invoices still remain open. This allows my sister-in-law to focus on training her clients knowing that all of the invoicing goals (noted above) have been met.
In a similar fashion, Heroku provides the necessary tooling to introduce messaging concepts into an existing project with little effort. Just like the Heroku service does the DevOps work for me, CloudAMQP provides a quick setup and allows me to turn my attention to building intellectual property within the Fitness application to yield a better application experience.
In the current state, my Heroku ecosystem for the Fitness application currently appears as shown below:
I look forward to building upon this design as development continues across the feature roadmap.
Have a really great day!

Written by johnjvester | Information Technology professional with 25+ years expertise in application design and architecture.
Published by HackerNoon on 2021/06/13