You have the following deployment options in Java EE (J2EE/5/6):
- Deploying an EAR / WAR to a multi-project or dedicated server
- Cluster deployment: deployment units get spread across the nodes, you get HTTPSession and @Stateful Bean failover
- Cluster deployment, but no replication or failover. You get easy distribution and management (deactivating, disabling) of deployment units
- Dynamic environment: your application is split across loose-coupled modules, which could be managed independently
- Minimization of moving parts - simplicity is the key!
- Very simple upgrade and maintenance strategies - the system should always be in consistent state.
- Dead simple monitoring and deployment - operations are rarely interested in our geeky frameworks, switches and whistles.
- Avoidance of third party frameworks and libraries - they are faster EOL than you may think.
- We deployed not only the application, but also the application server with already installed application AND JDK as a zip with hardcoded classpath
- The application consisted with as few as only possible external libraries, parts or frameworks. We started with JDK 1.6 then expanded the search to Java EE 6 (or lower versions)
- Although we had logical packages and components, we tried to deploy the application as a "BLOB" e.g. maven-with-dependencies does a nice job here. The fewer jars you have - the less can go wrong.
- We didn't used a cluster but also didn't relied on the HA-qualities of the HTTPSession and SFSB in particular.
- A simple load balancer, sometimes a slightly more sophisticated strategy, distributed the load across mostly independent servers (with sticky sessions were possible).
In real world you have a lot more to consider (especially the other non-functional requirements, politics and policies), than only availability and criticality.
The strategy above has an interesting side-effect: you can not only upgrade your application, but also your appserver AND the JDK, just by replacing a folder or extracting a zip. After re-routing the traffic and killing the process or virtual machine - you can consistently and atomically update not only the app, but also the whole environment. Interestingly a "mission critical" strategy is fully compatible with building maintainable software.
[See chapter 1 in "Real World Java EE Patterns - Rethinking Best Practices" for more in depth transactions, clustering, state, optimistic concurrency discussion]