Improve availability in Java enterprise applications

Best practices for designing highly available Java enterprise applications

It is always the middle of the day in some part of the world. For an online business, operating hours are 24 hours, seven days a week. This means maintaining uninterrupted uptime is of primary importance to commercial Websites. Any application outage means customers lack access to information or services offered by the business. This results in potential loss of revenue and dissatisfied customers.

The most obvious step for improving the availability of applications running high-traffic Websites is to host them in environments designed for high availability. Availability in the infrastructure is typically achieved by building redundancies at all levels. But to fully leverage the availability built in an environment, application designers must design their applications with high availability in mind. It is important to be conscious of the fact that certain application design choices that would work well in a simple environment may not be as effective in a highly available environment. For applications that do not have the luxury of being deployed in a highly available environment, some design considerations can improve their availability.

This article sets the stage by briefly discussing the characteristics of a highly available (HA) environment. Then, it discusses special design considerations for applications deployed in HA environments. And finally, it introduces application-level best practices for improving availability even if an application is not being deployed in an HA environment.

Highly available environments

Below are two illustrations of hosting environments that represent the two ends of the HA infrastructures spectrum. Figure 1 shows a complete Java EE (Java Platform, Enterprise Edition) delivery environment hosted entirely on one server.

Figure 1. Single-server hosting environment

In this type of environment, the HTTP, Java EE, and RDBM (relational database management) servers, all coexist on the same physical machine. This is the most cost-effective Java EE environment possible, but also the least fault-tolerant because every infrastructure component is a potential single point of failure.

On the other end of the spectrum is an infrastructure as depicted in Figure 2.

Figure 2. High availability hosting environment. Click on thumbnail to view full-sized image.

This deployment environment consists of multiple data centers, typically in different geographic locations, and each identical in hardware and software levels. Almost everything about them is redundant down to their connections to different Internet backbones. In this environment, each infrastructure component is housed on a separate piece of fault-tolerant hardware. These geographically disparate data delivery environments rely on both data and cluster synchronization so each data center can field requests simultaneously.

The two most important characteristics of HA environments are:

  1. No single point of failure: Build redundancy in the environment at all tiers to remove single points of failure. This tenet should be applied to both hardware and the infrastructure's software components.
  2. Failover: In the case of a failure, use load balancing to dynamically restore service to application servers.

Design applications to leverage high availability in the infrastructure

Having an HA infrastructure does not by itself guarantee high availability for an application. If an application is not designed to leverage high availability in the infrastructure, it will not achieve its availability goals. Applications designed for these types of environments need special design and implementation considerations.

For instance, how do applications share session information across data delivery centers or how does writing to replicated databases affect your end-user's overall experience. The following design and implementation best practices help leverage HA infrastructure and increase application availability:

  • Use read-only JVM caches: In a typical HA configuration, several instances of an application run simultaneously on different application servers, each with its own Java virtual machine. Since each server has its own JVM, if you update data in a local cache, the effects of the update will only be seen by that JVM. Other instances of the application will not be aware of this update, which is why you should treat JVM-level caches as read-only. The exception is if there is a distributed cache that keeps the data synchronized in real time across the JVMs. Depending on your application server, you may have a custom solution for this problem or implement an RMI (remote method invocation) solution on your own.
  • Considerations for distributed HTTP sessions: To overcome the stateless nature of the HTTP protocol, Web applications rely on the HttpSession API to store session information. In a clustered environment, where several instances of the application run on different application servers, the following should be considered while using the HttpSession API:

    • Replicate session data in real time: Since you will not know which instance of your application will field the next request in a session, session data should be made available to all servers participating in a cluster in real time. This can be achieved by storing the session data in a database and then enabling replication at the database level. Some application servers, e.g., WebSphere, WebLogic, provide a faster—in memory—session data replication feature. If that feature is available in your application server, you should consider using it.
    • Serialize your session objects: Objects stored in distributed sessions need to implement the interface. Implementing this interface ensures the data can be transported over the "wire" to each server instance in the cluster.

      A good practice is to enforce the use of a custom method like the addObjectToSession(String key, Serializable value) method, instead of the default HttpSession.setAttribute (String key, Object value) method when adding session data. The distinction is, if you were to call the addObjectToSession() method with a non-serializable object, you would see a compile-time error. If you were to try and replicate a session object that had non-seriablizable objects placed into session with the put() method, you would see a runtime error and potentially, a broken user experience.

    • Implement failover within your application: Make the application aware of redundant infrastructure resources so, if needed, a dynamic runtime switch is possible. For example, if your application uses a messaging server and has one messaging server in each data center, make the application running in a given data center aware that another server in a different data center can be used as a secondary messaging server in case of failure.
    • Be creative when testing your application: Ideally, you would test your application in an environment that exactly matches your deployment environment. In the real world, an HA production environment is usually much more elaborate than your staging and testing environments; thus, exactly mirroring your production environment is often cost prohibitive.

      If this is your situation, you can compensate for your lack of infrastructure while testing by being creative with your testing approach. You may be able to mimic your HA environment with two workstations with vertically stacked software components (HTTP, database, and application server all on one physical machine).

      After having done your best to emulate your HA environment, you should consider the following test scenarios: Does your application behave when you shut down one of the databases? Does your application successfully employ the LDAP (lightweight directory access protocol) server from the other machine when the local one fails to respond? These are the types of tests that will help test the application's tolerance to outages.

Application-level best practices for improving availability

So far, this article has focused on the importance of availability, how an infrastructure can be made available, and considerations for designing applications for an HA environment. This next section introduces application-level design techniques that can improve availability.

Autonomous application resource management

Externalizing resources to avoid hard-coding them in the application is a common practice. Typically, the externalized resources are loaded into memory by the application during startup and then used by the application in a read-only fashion. Examples of externalized resource files are properties files that contain translatable strings (typically used by Java Internationalization) or configuration files that contain application-related configuration information.

Under normal circumstances, changes to externalized resources would not be reflected in the application's memory. To make the application aware of resource updates, the application typically needs to be restarted. Each time the application is restarted, it temporarily stops servicing requests. In this scenario, an application's availability and how frequently its resources are updated are inversely related.

An effective way to increase an application's availability is to automate the reload of all externalized resources, without shutting down or restarting the application. The following are some salient features of an autonomous application resource manager utility along with general guidance on its design and implementation.

The resource manager:

  • Polls resources periodically to check for resource updates and reloads a resource only if it has changed. The polling period can vary for different types of resources depending on business needs.
  • Provides a means to issue a user-initiated explicit reload, which could be triggered via a browser; e.g., a reload servlet could handle this request and do the reload.
  • Makes the reload operation thread-safe to ensure that the application obtains the most current configuration data during retrieval, even during a reload operation. Accessing any resource while it is being reloaded could result in inconsistent data.
  • Supports heterogeneous resource types, such as properties files and XML files.
  • Can use open source utilities, such as Quartz Scheduler and Apache Commons Configuration packages.

A sample design for the resource manager could maintain two copies of each application resource to ensure that, while a particular resource is being refreshed in memory, the application can still query the copy to obtain any required information, thus maintaining application availability during the reload operation. Figure 3 is a schematic representation of this design:

Figure 3. Sample resource manager model. Click on thumbnail to view full-sized image.

When the application is initialized, the resource manager will load into memory all designated resources that could be specified in an externalized properties file. A generic Resource interface can be implemented to create a unique Resource type corresponding to each application resource. The ResourceContainer type contains references to a registry of ResourceController objects, each of which contains two objects of any particular Resource type. Only one of these two objects is active at a given point in time. Resource data is accessed via a generic get() method on the ResourceContainer interface. When an application issues a request, the ResourceController object queries the active Resource instance. Also, during an auto-refresh or explicit user-initiated refresh operation for any particular resource, provided the resource has been modified since the last refresh, the ResourceController reloads the inactive instance, then designates it as the active Resource object, and nullifies the formerly active one.

In this way, the resource manager utility ensures that application resources are current, complete, and updated automatically in a thread-safe way, without restarting the application and thus preventing any downtime or reduced application availability.

Graceful error handling

An important aspect of maintaining application availability is to avoid errors. But, if errors do happen, then an approach where the users see a degraded experience rather than an error page is a good strategy.

More approaches to graceful error handling:

  • Possibly use default content in the error page: Defining default pages for different scenarios should be considered. Based on which scenario has an error, the default page for that scenario can be shown. Being able to use default behavior depends a lot on the nature of the application and the function being executed when an error occurs. For example, in one of our applications, we were able to define a generic landing page as the error page.
  • When an error happens, alerts should be sent.
  • Configure a catch-all error at the Web-container level. This can be done by defining an error page in web.xml that helps in failing gracefully.

Monitor application health

It is a good idea to define some tests that verify that the application is in good health. These tests can be wrapped in a servlet that can be invoked either manually or by automated tools.

Some approaches to monitoring the application's health follow:

1 2 Page 1
Page 1 of 2
How to choose a low-code development platform