BigBank is a loan processing application that showcases how Fabric3 greatly simplifies the development of integration applications. Fabric3 provides a way to create highly reliable applications without costly middleware and complex configuration. It enables developers to leverage multiple transports for distributed communications (not all remote communications should be done using WS-* or REST), advanced asynchronous messaging, and eventing patterns using straightforward Java idioms and APIs. Furthermore, Fabric3 offers a solution for application modularity where components can be composed into loosely-coupled subsystems.
BigBank demonstrates how to use each of these features in practice:
- Remote Communications: Expose services over multiple transports including Web Services, REST/HTTP, JMS, ZeroMQ (high-speed messaging).
- Asynchronous Messaging: Create non-blocking, one-way service invocations with callbacks for high application throughput.
- Modularity: Modularize applications by grouping components into composites.
- Reliability: Greatly reduce the complexity associated with writing highly reliable code by using timer components and clustered singletons.
- Pub/Sub Eventing: Build event-based systems for loose-coupling.
- Simplicity: Do all of this without the burden of complex configuration and proprietary APIs.
The following diagram illustrates the BigBank system architecture:
As shown in the diagram, BigBank is consists of a number of services, some of which are tightly-coupled and collocated (in-process) and others which may be distributed and are loosely coupled. This reflects a common choice when designing distributed architectures: creating loosely coupled, distributed services allows for re-use and high-availability at the cost of greater complexity and performance. Therefore, not all services should be loosely coupled or distributed.
Similarly, BigBank makes choices about when to use specific remote transports. Web Services and REST/HTTP provide greater interoperability and looser coupling as they do not place minimal technology requirements on clients. Hence, they are used at the edges of the system, in this case to receive loan applications from third-parties.
However, Web Services and REST/HTTP are more complex to code and less performant than transports such as JMS or ZeroMQ. Since BigBank controls the internal services of its application and can therefore dictate certain technology requirements, interoperability is less of a concern. As a consequence, BigBank is architected to rely on JMS or ZeroMQ for remote communication between its non-edge services. For example, when deployed to a clustered environment, communication beween the LoanService and RatingService shown in the previous diagram is performed using JMS or ZeroMQ.
We now turn to a detailed description of the application.
The following modifies the previous architecture diagram to illustrate the steps taken to process a loan application:
- Step 1: A loan application is received by the LoanService over Web Services, REST/HTTP, or via the file system. Note the application is received in the context of a transaction.
- Step 2: The LoanService persists the application using JPA.
- Step 3: The LoanService sends an asynchronous request to the RatingService. Since a transaction is active, the message will be transactionally enqueued if JMS is used as the remote transport. Then, as this operation is one-way, control returns to the LoanService immediately, the transaction is committed, and communication with the client submitting the application is terminated.
- Step 4: The RatingService delegates to the CreditService to obtain credit scores from third-party sources (in the sample, these are stubbed out).
- Step 5: After tiering the application, the RatingService makes an asynchronous callback to the LoanService to resume processing.
- Step 6: The LoanService resume processing by invoking the RiskService to assess risk and either accept or reject the application.
- Step 7: The LoanServices persists the risk information and finalizes the application. At this point the client may be notified that the application is complete.
The LoanService component is responsible for application intake and processing. After an application has been received, the LoanService delegates to the RatingService to perform credit checks and tier the application. When the RatingService has completed, the LoanService resumes processing by assessing risk and either approving or rejecting the application.
Let's now look more closely at how the LoanService receives applications from external clients.
One of the key advantages of Fabric3 is that it makes it possible to expose a component over multiple transports without complex configuration or polluting application code with (proprietary) APIs. The LoanService is setup to receive applications over Web Services, REST/HTTP and file-system messages where files are placed in a directory which is polled. It is worth noting that while Web Services and REST/HTTP are synchronous request-response, the file-system "binding" is purely asynchronous. That is, the client issues a request and a response is returned to a separate directory at a later point in time. The LoanService has been designed to handle both styles of communication.
Exposing the LoanService over multiple bindings is done in the loan composite (META-INF/loan.composite) using binding elements:
When deployed, Fabric3 will create endpoints over the configured binding transports. When an endpoint is called, Fabric3 will invoke a method on the component implementation. For example, when a file is received in receive.queue directory, the LoanServiceImpl.process() method is called:
The file binding works by passing an InputStream for the file, which is then used to deserialize the loan application message using JAXB. Passing an InputStream allows streaming parsers such a StAX to be used on large messages, where they can be processed without loading the entire message content into memory.
The REST and Web Services bindings perform deserialization of XML and JSON payloads automatically. The following excerpt shows the LoanServiceImpl.apply() method used to receive a loan application over web services:
The RatingService receives requests from the LoanService to tier an application based on credit scores obtained from external bureaus. In a distributed environment, the RatingService is deployed to a second cluster (in Fabric3 terms, a "zone"), which can be horizontally scaled independent of the loan service. In more complex environments, the RatingService may be deployed in a separate contribution from the LoanService. This would allow the two services to be versioned and maintained independently.
The RatingService demonstrates two of the most important Fabric3 features: asynchronous operations and composition. We discuss those in detail now.
Asynchronous operations allow clients to invoke a service and have control return immediately, without having to wait for the service to complete processing. When the client and service are collocated in the same process, asynchrony is achieved by executing the invoked service operation on a separate thread. When the client and service are remote, asynchrony is provided by the underlying communications transport, for example, a JMS provider or ZeroMQ.
Asynchrony is useful in a number of instances:
- Applications requiring high-throughput can use asynchronous operations to avoid blocking clients and tying up runtime threads waiting for processing to complete. This is particularly valuable when an operation involves latency or takes a long time to finish.
- To provide loose coupling between services. In distributed scenarios, asynchronous operations can be used to allow parts of a system to be versioned and maintained independently without impacting application availability.
The RatingService uses an asynchronous (one-way) operation for both purposes:
The @OneWay annotation instructs Fabric3 to perform all invocations in a non-blocking fashion. When the LoanService invokes the rate() method, control is returned immediately. This frees clients (and runtime threads) from blocking while the RatingService contacts third-party credit bureaus.
The RatingService also uses a one-way operation for system maintainability. For example, if a reliable JMS provider were used for remote communications between the LoanService and RatingService, the latter could be temporarily taken offline for maintenance. Requests from the LoanService would queue in the JMS provider until the RatingService came back online and started receiving messages.
SCA provides application modularity through a combination of OSGi (for Java package resolution) and composites. A composite file is used to define components, which in turn can be used as a component in a higher-level composite. The rate composite (META-INF/rate.composite) uses a credit service composite:
The credit service composite in turn defines three components:
The service element in the above example "promotes" the CreditService as the interface for the composite. When the composite is used as a component in the rate composite, it provides one service that is wired to the RatingService.
By composing composites in this manner, it is possible to hide implementation details from other parts of an application and provide clear contract for clients.
Making a system reliable is often a complex task, particularly if an application relies on external services. For example, the credit bureau services could involve a web service call which at times may fail. Two approaches are generally used to address these issues:
- All remote calls are wrapped with retry logic and communication exceptions caught.
- A JMS or reliable messaging provider is used at various points in the system. For example, the RatingService would transactionally dequeue messages and attempt web service calls to the credit bureaus. Any raised exceptions are bubbled up, causing the transactional dequeue to fail and the messaging provider to redeliver at a later point in time.
Having to account for the possibility of failure for all remote invocations at different points in a system is error-prone and unnecessarily increases code complexity. While reliable messaging providers are often a good solution, they are complex to setup, expensive, impact performance, and are not helpful for in-process asynchronous invocations.
BigBank uses a simple solution to provide guaranteed processing: timer components. When a loan application is initially received, it is transactionally persisted using JPA to a database prior to returning an acknowledgement to the client. The loan application may be in a number of predefined states, which is tracked in the database. At any point, subsequent processing can fail and the BigBank application lets the exception propagate until it is handled at an appropriate layer in the application.
Instead of inserting retry and recovery logic at any point a failure can occur, BigBank centralizes recovery in a timer component, the RecoveryManager. The RecoveryManager is fired periodically and queries the database for "stuck" applications, for example, an application that has not received a credit score for a period of time. When it discovers such an application, it calls the LoanService to retry processing. The implementation is fairly straightforward:
There two things to note. First, the @ManagedTransaction annotation instructs Fabric3 to fire the timer in the context of a transaction. The second is the use of the @Scope annotation. In this case, the timer is domain scoped, which means that Fabric3 guarantees only one instance to be active in a cluster at a time (a clustered singleton). Two instances should not be performing the same recovery operation. If a runtime hosting the timer instance fails, the Fabric3 cluster will automatically select another member to host the timer and fire it.
BigBank also makes use of pub/sub eventing for auditing and statistics purposes. When an application is received, the LoanService fires an event to the loan channel. An AuditComponent consumes events from the channel and records changes to applications. An ApplicationStatisticsComponent is also attached to the channel and calculates various running averages which are then periodically published to a second channel. Publishing and subscribing to channels works in the same way component references are wired to services. The following example illustrates how the AuditComponent consumes events from the loan channel:
The next section covers how to deploy the BigBank application to a multi-cluster environment.