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:
Below, is a current version of the feature roadmap:
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.
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:
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:
At this point, each InvoiceDto is available for the trainer to view.
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.
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.
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.
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!
Also published at https://dzone.com/articles/leveraging-cloudamqp-within-my-heroku-based-saas-s