Showing posts with label j2ee. Show all posts
Showing posts with label j2ee. Show all posts

Monday, September 23, 2019

Design patterns - no more

Design patterns - choose good programming language without internal "philosophical" gaps, then you don't need to learn patterns!

Yes, I admit I have aversion to patterns, for a simple reason - if programming language that you choose is good ... you don't need patterns, you don't need workarounds to cover language issues.

In my opinion patterns are just workarounds/cover-ups for language gaps/inconsistency.
It looks like: you build the house on the tree and you forgot to put a ladder, so you cannot reach the doors. Then comes patterns: use ropes, build concrete staircases, etc...
If you spend time on planing tree-house in the first place - no need for workarounds.

Just to be clear yes I'm talking about java - if you want to write a good program you need to know the whole catalog of patterns (starting with GOF). If you compare it to python, scala (tobe checked: kotlin, swift) - no more patterns. There are some suggested good behaviors like use functional instead of OO, but that is your choice/ your style.

I think this is an advantage of new languages - if you want to promote a new language you need to have it clean and easy to learn and use.




Friday, December 6, 2013

Java: Code review tools

Rietveld - code review tool running on Google App Engine, for use with Subversion .

Gerrit2 - Java solution for use with GIT.

TODO:

http://phabricator.org/

http://www.reviewboard.org/


Rietveld - code review tool running on Google App Engine, for use with Subversion .

Rietveld is in common use by many open source projects, facilitating their peer reviews much as Mondrian does for Google employees. Unlike Mondrian and the Google Perforce triggers, Rietveld is strictly advisory and does not enforce peer-review prior to submission.

Git is a distributed version control system, wherein each repository is assumed to be owned/maintained by a single user. There are no inherit security controls built into Git, so the ability to read from or write to a repository is controlled entirely by the host's filesystem access controls. When multiple maintainers collaborate on a single shared repository a high degree of trust is required, as any collaborator with write access can alter the repository.

Gitosis provides tools to secure centralized Git repositories, permitting multiple maintainers to manage the same project at once, by restricting the access to only over a secure network protocol, much like Perforce secures a repository by only permitting access over its network port.

Gerrit Code Review started as a simple set of patches to Rietveld, and was originally built to service AOSP. This quickly turned into a fork as we added access control features that Guido van Rossum did not want to see complicating the Rietveld code base. As the functionality and code were starting to become drastically different, a different name was needed.

Gerrit2 is a complete rewrite of the Gerrit fork, completely changing the implementation from Python on Google App Engine, to Java on a J2EE servlet container and a SQL database.


Friday, October 18, 2013

Batch processing strategies

To help design and implement batch systems, basic batch application building blocks and patterns should be provided to the designers and programmers in form of sample structure charts and code shells. When starting to design a batch job, the business logic should be decomposed into a series of steps which can be implemented using the following standard building blocks:

  • Conversion Applications: For each type of file supplied by or generated to an external system, a conversion application will need to be created to convert the transaction records supplied into a standard format required for processing. This type of batch application can partly or entirely consist of translation utility modules (see Basic Batch Services).
  • Validation Applications: Validation applications ensure that all input/output records are correct and consistent. Validation is typically based on file headers and trailers, checksums and validation algorithms as well as record level cross-checks.
  • Extract Applications: An application that reads a set of records from a database or input file, selects records based on predefined rules, and writes the records to an output file.
  • Extract/Update Applications: An application that reads records from a database or an input file, and makes changes to a database or an output file driven by the data found in each input record.
  • Processing and Updating Applications: An application that performs processing on input transactions from an extract or a validation application. The processing will usually involve reading a database to obtain data required for processing, potentially updating the database and creating records for output processing.
  • Output/Format Applications: Applications reading an input file, restructures data from this record according to a standard format, and produces an output file for printing or transmission to another program or system.
Additionally a basic application shell should be provided for business logic that cannot be built using the previously mentioned building blocks.
In addition to the main building blocks, each application may use one or more of standard utility steps, such as:
  • Sort - A Program that reads an input file and produces an output file where records have been re-sequenced according to a sort key field in the records. Sorts are usually performed by standard system utilities.
  • Split - A program that reads a single input file, and writes each record to one of several output files based on a field value. Splits can be tailored or performed by parameter-driven standard system utilities.
  • Merge - A program that reads records from multiple input files and produces one output file with combined data from the input files. Merges can be tailored or performed by parameter-driven standard system utilities.
Batch applications can additionally be categorized by their input source:
  • Database-driven applications are driven by rows or values retrieved from the database.
  • File-driven applications are driven by records or values retrieved from a file.
  • Message-driven applications are driven by messages retrieved from a message queue.
The foundation of any batch system is the processing strategy. Factors affecting the selection of the strategy include: estimated batch system volume, concurrency with on-line or with another batch systems, available batch windows (and with more enterprises wanting to be up and running 24x7, this leaves no obvious batch windows).
Typical processing options for batch are:
  • Normal processing in a batch window during off-line
  • Concurrent batch / on-line processing
  • Parallel processing of many different batch runs or jobs at the same time
  • Partitioning (i.e. processing of many instances of the same job at the same time)
  • A combination of these
The order in the list above reflects the implementation complexity, processing in a batch window being the easiest and partitioning the most complex to implement.
Some or all of these options may be supported by a commercial scheduler.
In the following section these processing options are discussed in more detail. It is important to notice that the commit and locking strategy adopted by batch processes will be dependent on the type of processing performed, and as a rule of thumb and the on-line locking strategy should also use the same principles. Therefore, the batch architecture cannot be simply an afterthought when designing an overall architecture.
The locking strategy can use only normal database locks, or an additional custom locking service can be implemented in the architecture. The locking service would track database locking (for example by storing the necessary information in a dedicated db-table) and give or deny permissions to the application programs requesting a db operation. Retry logic could also be implemented by this architecture to avoid aborting a batch job in case of a lock situation.

1. Normal processing in a batch window
For simple batch processes running in a separate batch window, where the data being updated is not required by on-line users or other batch processes, concurrency is not an issue and a single commit can be done at the end of the batch run.
In most cases a more robust approach is more appropriate. A thing to keep in mind is that batch systems have a tendency to grow as time goes by, both in terms of complexity and the data volumes they will handle. If no locking strategy is in place and the system still relies on a single commit point, modifying the batch programs can be painful. Therefore, even with the simplest batch systems, consider the need for commit logic for restart-recovery options as well as the information concerning the more complex cases below.

2. Concurrent batch / on-line processing
Batch applications processing data that can simultaneously be updated by on-line users, should not lock any data (either in the database or in files) which could be required by on-line users for more than a few seconds. Also updates should be committed to the database at the end of every few transaction. This minimizes the portion of data that is unavailable to other processes and the elapsed time the data is unavailable.
Another option to minimize physical locking is to have a logical row-level locking implemented using either an Optimistic Locking Pattern or a Pessimistic Locking Pattern.
  • Optimistic locking assumes a low likelihood of record contention. It typically means inserting a timestamp column in each database table used concurrently by both batch and on-line processing. When an application fetches a row for processing, it also fetches the timestamp. As the application then tries to update the processed row, the update uses the original timestamp in the WHERE clause. If the timestamp matches, the data and the timestamp will be updated successfully. If the timestamp does not match, this indicates that another application has updated the same row between the fetch and the update attempt and therefore the update cannot be performed.
  • Pessimistic locking is any locking strategy that assumes there is a high likelihood of record contention and therefore either a physical or logical lock needs to be obtained at retrieval time. One type of pessimistic logical locking uses a dedicated lock-column in the database table. When an application retrieves the row for update, it sets a flag in the lock column. With the flag in place, other applications attempting to retrieve the same row will logically fail. When the application that set the flag updates the row, it also clears the flag, enabling the row to be retrieved by other applications. Please note, that the integrity of data must be maintained also between the initial fetch and the setting of the flag, for example by using db locks (e.g., SELECT FOR UPDATE). Note also that this method suffers from the same downside as physical locking except that it is somewhat easier to manage building a time-out mechanism that will get the lock released if the user goes to lunch while the record is locked.
These patterns are not necessarily suitable for batch processing, but they might be used for concurrent batch and on-line processing (e.g. in cases where the database doesn't support row-level locking). As a general rule, optimistic locking is more suitable for on-line applications, while pessimistic locking is more suitable for batch applications. Whenever logical locking is used, the same scheme must be used for all applications accessing data entities protected by logical locks.
Note that both of these solutions only address locking a single record. Often we may need to lock a logically related group of records. With physical locks, you have to manage these very carefully in order to avoid potential deadlocks. With logical locks, it is usually best to build a logical lock manager that understands the logical record groups you want to protect and can ensure that locks are coherent and non-deadlocking. This logical lock manager usually uses its own tables for lock management, contention reporting, time-out mechanism, etc.

3. Parallel Processing
Parallel processing allows multiple batch runs / jobs to run in parallel to minimize the total elapsed batch processing time. This is not a problem as long as the jobs are not sharing the same files, db-tables or index spaces. If they do, this service should be implemented using partitioned data. Another option is to build an architecture module for maintaining interdependencies using a control table. A control table should contain a row for each shared resource and whether it is in use by an application or not. The batch architecture or the application in a parallel job would then retrieve information from that table to determine if it can get access to the resource it needs or not.
If the data access is not a problem, parallel processing can be implemented through the use of additional threads to process in parallel. In the mainframe environment, parallel job classes have traditionally been used, in order to ensure adequate CPU time for all the processes. Regardless, the solution has to be robust enough to ensure time slices for all the running processes.
Other key issues in parallel processing include load balancing and the availability of general system resources such as files, database buffer pools etc. Also note that the control table itself can easily become a critical resource.

4. Partitioning
Using partitioning allows multiple versions of large batch applications to run concurrently. The purpose of this is to reduce the elapsed time required to process long batch jobs. Processes which can be successfully partitioned are those where the input file can be split and/or the main database tables partitioned to allow the application to run against different sets of data.
In addition, processes which are partitioned must be designed to only process their assigned data set. A partitioning architecture has to be closely tied to the database design and the database partitioning strategy. Please note, that the database partitioning doesn't necessarily mean physical partitioning of the database, although in most cases this is advisable.
The architecture should be flexible enough to allow dynamic configuration of the number of partitions. Both automatic and user controlled configuration should be considered. Automatic configuration may be based on parameters such as the input file size and/or the number of input records.

4.1 Partitioning Approaches
The following lists some of the possible partitioning approaches. Selecting a partitioning approach has to be done on a case-by-case basis.

1. Fixed and Even Break-Up of Record Set
This involves breaking the input record set into an even number of portions (e.g. 10, where each portion will have exactly 1/10th of the entire record set). Each portion is then processed by one instance of the batch/extract application.
In order to use this approach, preprocessing will be required to split the recordset up. The result of this split will be a lower and upper bound placement number which can be used as input to the batch/extract application in order to restrict its processing to its portion alone.
Preprocessing could be a large overhead as it has to calculate and determine the bounds of each portion of the record set.

2. Breakup by a Key Column
This involves breaking up the input record set by a key column such as a location code, and assigning data from each key to a batch instance. In order to achieve this, column values can either be

3. Assigned to a batch instance via a partitioning table (see below for details).
4. Assigned to a batch instance by a portion of the value (e.g. values 0000-0999, 1000 - 1999, etc.)
Under option 1, addition of new values will mean a manual reconfiguration of the batch/extract to ensure that the new value is added to a particular instance.
Under option 2, this will ensure that all values are covered via an instance of the batch job. However, the number of values processed by one instance is dependent on the distribution of column values (i.e. there may be a large number of locations in the 0000-0999 range, and few in the 1000-1999 range). Under this option, the data range should be designed with partitioning in mind.
Under both options, the optimal even distribution of records to batch instances cannot be realized. There is no dynamic configuration of the number of batch instances used.

5. Breakup by Views
This approach is basically breakup by a key column, but on the database level. It involves breaking up the recordset into views. These views will be used by each instance of the batch application during its processing. The breakup will be done by grouping the data.
With this option, each instance of a batch application will have to be configured to hit a particular view (instead of the master table). Also, with the addition of new data values, this new group of data will have to be included into a view. There is no dynamic configuration capability, as a change in the number of instances will result in a change to the views.

6. Addition of a Processing Indicator
This involves the addition of a new column to the input table, which acts as an indicator. As a preprocessing step, all indicators would be marked to non-processed. During the record fetch stage of the batch application, records are read on the condition that that record is marked non-processed, and once they are read (with lock), they are marked processing. When that record is completed, the indicator is updated to either complete or error. Many instances of a batch application can be started without a change, as the additional column ensures that a record is only processed once.
With this option, I/O on the table increases dynamically. In the case of an updating batch application, this impact is reduced, as a write will have to occur anyway.

7. Extract Table to a Flat File
This involves the extraction of the table into a file. This file can then be split into multiple segments and used as input to the batch instances.
With this option, the additional overhead of extracting the table into a file, and splitting it, may cancel out the effect of multi-partitioning. Dynamic configuration can be achieved via changing the file splitting script.

8. Use of a Hashing Column
This scheme involves the addition of a hash column (key/index) to the database tables used to retrieve the driver record. This hash column will have an indicator to determine which instance of the batch application will process this particular row. For example, if there are three batch instances to be started, then an indicator of 'A' will mark that row for processing by instance 1, an indicator of 'B' will mark that row for processing by instance 2, etc.
The procedure used to retrieve the records would then have an additional WHERE clause to select all rows marked by a particular indicator. The inserts in this table would involve the addition of the marker field, which would be defaulted to one of the instances (e.g. 'A').
A simple batch application would be used to update the indicators such as to redistribute the load between the different instances. When a sufficiently large number of new rows have been added, this batch can be run (anytime, except in the batch window) to redistribute the new rows to other instances.
Additional instances of the batch application only require the running of the batch application as above to redistribute the indicators to cater for a new number of instances.

4.2 Database and Application design Principles
An architecture that supports multi-partitioned applications which run against partitioned database tables using the key column approach, should include a central partition repository for storing partition parameters. This provides flexibility and ensures maintainability. The repository will generally consist of a single table known as the partition table.
Information stored in the partition table will be static and in general should be maintained by the DBA. The table should consist of one row of information for each partition of a multi-partitioned application. The table should have columns for: Program ID Code, Partition Number (Logical ID of the partition), Low Value of the db key column for this partition, High Value of the db key column for this partition.
On program start-up the program id and partition number should be passed to the application from the architecture (Control Processing Tasklet). These variables are used to read the partition table, to determine what range of data the application is to process (if a key column approach is used). In addition the partition number must be used throughout the processing to:
  • Add to the output files/database updates in order for the merge process to work properly
  • Report normal processing to the batch log and any errors that occur during execution to the architecture error handler
4.3 Minimizing Deadlocks When applications run in parallel or partitioned, contention in database resources and deadlocks may occur. It is critical that the database design team eliminates potential contention situations as far as possible as part of the database design.
Also ensure that the database index tables are designed with deadlock prevention and performance in mind.
Deadlocks or hot spots often occur in administration or architecture tables such as log tables, control tables, and lock tables. The implications of these should be taken into account as well. A realistic stress test is crucial for identifying the possible bottlenecks in the architecture.
To minimize the impact of conflicts on data, the architecture should provide services such as wait-and-retry intervals when attaching to a database or when encountering a deadlock. This means a built-in mechanism to react to certain database return codes and instead of issuing an immediate error handling, waiting a predetermined amount of time and retrying the database operation.

4.4 Parameter Passing and Validation
The partition architecture should be relatively transparent to application developers. The architecture should perform all tasks associated with running the application in a partitioned mode including:
  • Retrieve partition parameters before application start-up
  • Validate partition parameters before application start-up
  • Pass parameters to application at start-up
The validation should include checks to ensure that:
  • the application has sufficient partitions to cover the whole data range
  • there are no gaps between partitions
If the database is partitioned, some additional validation may be necessary to ensure that a single partition does not span database partitions.
Also the architecture should take into consideration the consolidation of partitions. Key questions include:
  • Must all the partitions be finished before going into the next job step?
  • What happens if one of the partitions aborts?

Friday, March 22, 2013

Java: Best Practices for Using Exceptions

The next set of best practices show how the client code should deal with an API that throws checked exceptions.

1. Always clean up after yourself


If you are using resources like database connections or network connections, make sure you clean them up. If the API you are invoking uses only unchecked exceptions, you should still clean up resources after use, with try - finally blocks.

public void dataAccessCode(){
    Connection conn = null;
    try{
        conn = getConnection();
        ..some code that throws SQLException
    }catch(SQLException ex){
        ex.printStacktrace();
    } finally{
        DBUtil.closeConnection(conn);
    }
}

class DBUtil{
    public static void closeConnection
        (Connection conn){
        try{
            conn.close();
        } catch(SQLException ex){
            logger.error("Cannot close connection");
            throw new RuntimeException(ex);
        }
    }
}

DBUtil is a utility class that closes the Connection. The important point is the use of finally block, which executes whether or not an exception is caught. In this example, the finally closes the connection and throws a RuntimeException if there is problem with closing the connection.

2. Never use exceptions for flow control


Generating stack traces is expensive and the value of a stack trace is in debugging. In a flow-control situation, the stack trace would be ignored, since the client just wants to know how to proceed.
In the code below, a custom exception, MaximumCountReachedException, is used to control the flow.
public void useExceptionsForFlowControl() {
    try {
        while (true) {
            increaseCount();
        }
    } catch (MaximumCountReachedException ex) {
    }
    //Continue execution
}

public void increaseCount()
    throws MaximumCountReachedException {
    if (count >= 5000)
        throw new MaximumCountReachedException();
}

The useExceptionsForFlowControl() uses an infinite loop to increase the count until the exception is thrown. This not only makes the code difficult to read, but also makes it slower. Use exception handling only in exceptional situations.

3. Do not suppress or ignore exceptions


When a method from an API throws a checked exception, it is trying to tell you that you should take some counter action. If the checked exception does not make sense to you, do not hesitate to convert it into an unchecked exception and throw it again, but do not ignore it by catching it with {} and then continue as if nothing had happened.

4. Do not catch top-level exceptions


Unchecked exceptions inherit from the RuntimeException class, which in turn inherits from Exception. By catching the Exception class, you are also catching RuntimeException as in the following code:

try{
..
}catch(Exception ex){
}
The code above ignores unchecked exceptions, as well.

5. Log exceptions just once


Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception. So just log it once.

Sunday, February 3, 2013

Performance Processing

Multi-threaded solutions: complete as much work as possible in the shortest time possible. Parallel processing using multiple threads is necessary to bring all available computing resources to bear on the problem. Today's multiple core, multiple CPU servers are especially well suited for multi-threaded processing.


 

The basic software architecture pattern to follow is a dispatcher-worker pattern. This pattern consists of a centralized controller (the dispatcher) that sends chunks of work to multiple worker threads. This basic pattern is often used in software applications that must do a high volume of work.

The central problem is to determine how to partition the chunks of work to maximize the efficiency of threads and database interactions. Clearly, each thread must operate on an independent logical unit of work. Otherwise, concurrent threads might end up waiting on one another or incorrectly altering results related to other threads. Each thread must be able to complete its job independently from all other threads.

Subsequently, the units of work should be designed to minimize database interactions because these are very expensive. They involve asking the database to retrieve some data (which could require spinning a physical hard drive) and then moving that data over a network to the JEE server.Minimizing the frequency and volume of these interactions is the single most important factor in JEE processing performance. There are many ways to minimize database access and optimize the necessary database interactions.

 

Only Get the Required Data

The easiest way to minimize database interactions is to carefully construct the algorithm to only get and operate on the data it really needs. This may seem obvious but layers such as Data Access Objects (DAOs) or perhaps an object model based on Hibernate tend to return fully populated objects rather than just the few fields required. It's often convenient to reuse an existing data layer that does these things, but only do so if the time required to retrieve the extra data is acceptable. Otherwise, create new DAOs or JDBC statements to get just the specific data required.

 

Only Do the Work Required for Each Run

Another way to avoid bringing back unnecessary data and doing pointless work is to create a configurable  process. Processes often do several different but related operations and not all of them are always necessary. A little extra development work is required to provide input parameters that allow certain operations to be switched off for certain runs, but avoiding unnecessary work can provide worthwhile performance improvements.

 

Only Work on Data that Has Changed

In this same vein of avoiding unnecessary work, it is often possible to implement a feature that tracks which data has changed (and requires new operations) and which data has not changed (and can safely be ignored). Depending on the rate of change of the data, ignoring unchanged values can lead to a large performance improvement.

 

Use Data Warehousing Techniques to Compress Data over Time

The size of the dataset can be further reduced by exploiting common data warehouse data modelling techniques such as the concept of slowly changing dimensions. Data warehouses are often modelled to contain dimension tables and fact tables. The dimension tables contain all the descriptive attributes on which data is sliced. Fact tables contain the actual aggregated data. For example, there may be a fact table containing order totals with a foreign key to a dimension table that captures the name of the salesperson for the order, allowing the creation of a report to slice order totals by salesperson.

Slowly changing dimensions and slowly changing facts are methods that can be used to compress the volume of this data if the data changes over time. The idea is to put date ranges on the dimensions and facts rather than repeating the same values for each date in the time period. For example, a salesperson's name could change over time if she gets married. Without date range effectiveness on this dimension, it is necessary to capture the name as it was at each run to preserve historical data even though the data most likely does not change often. This is repetitive and wasteful. If the dimension has a date range, then the batch process need only store a row for each different value.

The same can be done with facts if the model requires storing facts at different times. If the result of the computation happens to be the same value as it was the last time, the process could just store a date range with the answer rather than storing the same answer multiple times.

 

Optimize Database Interactions

Eliminating unnecessary work is the best way to limit database interactions, but, clearly, some interactions must happen. Further strategies can be used to make sure those interactions are as efficient as possible.

 

Caching

One approach is to take advantage of caching technologies. Processes often require access to some set of master data that is reused throughout the process. This master data should be loaded from the database just once and then cached in memory within the application server context and reused.

This can be done using singletons or static variables that hold the data, or caching tools like JBoss Cache, GigaSpaces, Tangosol Coherence, etc. These latter tools provide benefits such as replicating the cached values across multiple JVM instances but introduce added complexity to the application.

One caveat for caching is that it may solve a database interaction problem but create a memory constraint problem because the in-memory cache in the application server tier may grow too large. RAM has become much cheaper in recent years, but most 32-bit JVMs are still limited to 2-4GB of heap space. Be careful that the cache will not exceed the memory space available and cause disk swapping on the application server. Also, consider 64-bit computing architectures that allow larger heap space.

 

Data Streaming

Another approach for optimizing database interactions is to favour a smaller number of de-normalized queries that retrieve large volumes of data rather than a larger number of more granular queries that retrieve small volumes of data. Relational databases are very good at creating an execution plan for a few complex queries and then streaming back the results as quickly as possible. They perform less well when asked to execute lots of small queries that appear to be randomly organized.

For example, consider a process that needs to compute the shipping cost on a large set of orders. You could choose to define each chunk of work to be a single order. The dispatcher could ask the database for the master list of order IDs and send each ID to a worker thread for processing. That worker thread could then ask the database for the details of each order, do its work, and save the answer back to the database.

To the database, this approach will feel like its getting slammed by lots of concurrent users asking for different orders all at the same time. There will be high contention for resources such as database connections and access to the order table. It will look sort of like a denial of service attack.

On the other hand, you could choose to define the chunks as an arbitrary number of orders, perhaps 1,000. The dispatcher could query the database for all required order columns rather than just the order ID. As each row is returned, the dispatcher would send all the order data required to compute shipping costs to a worker. Each worker would not have to query the database to do its work because everything it needs is provided as input.

As each worker completes its work, it would place the results on a persistence queue rather than immediately sending an individual insert or update to the database. Every time this queue reaches 1,000 entries, the process would send a bulk insert/update statement to the database.

The result is that the database is allowed to do a few, high-volume things as fast as it is able, rather than swapping between numerous small tasks.

 

Optimize Physical Database Access

Databases often respond slowly when they receive multiple requests that contend for data located on the same physical media. Avoiding this contention will speed the batch process. It's often possible to specify how database tables are segregated on different physical disk drives and divide tables that are likely to receive large numbers of concurrent requests onto different physical drives.

 

Use Database Tricks

Relational databases offer many configuration options and interaction methods that can be used intelligently to optimize a process. Performance monitoring tools should be used to watch the behavior of the database as the  process runs. This will allow the optimal configuration of settings such as how much memory to commit to the database's shared cache.

 

Transactions

A database uses transactions to group multiple changes into a single logical unit of work. These changes are then all committed and stored or all rolled back and thrown away together. The database must maintain a log of these changes to keep track of which things belong together. Large transactions result in a large transaction log. Large logs can negatively impact performance. There are a couple ways to avoid this problem. You could choose not to use transactions at all. Most databases include an autocommit feature that allows all changes to be committed immediately. Alternatively, you could make sure that each thread independently commits its own relatively small transaction. In any case, it's not wise to have large, long-running transactions as part of a process.

 

Prepared Statements

Most databases support the idea of precompiled SQL statements called prepared statements. A prepared statement is a SQL statement with placeholders for parameters that will be supplied later on with actual data values. The statement can be compiled once and then reused even if the parameters change. This saves compilation time on the database platform and improves performance.

Processes usually involve multiple executions of the same SQL statements over and over with different parameter values. This is a perfect situation for prepared statements. Dynamic statements should always be avoided.

 

Application Server Clustering

One of the benefits of using the JEE platform for processing is that you can leverage its ability to cluster multiple application servers. If JMS is used as the transport mechanism to move messages from the dispatcher to the workers and the JMS implementation supports clustered, distributed queues (as many do), the workers can reside on different physical machines. This provides a method to scale the performance of the batch process by adding application servers. A powerful cluster can be assembled using multiple, inexpensive commodity application servers and it can grow with the requirements of the batch process.

 


This is "default" performance processing strategies, in next article I will try to share with you personal view on creating ideal architecture for hig-performance processing, building engines, and possibilities of expansion through introducing CUDA, or ZooKeeper ;-)

 

Thursday, August 30, 2012

The top Java EE best practices

The best of the best practices
  1. Always use MVC.
  2. Don't reinvent the wheel.
  3. Apply automated unit tests and test harnesses at every layer.
  4. Develop to the specifications, not the application server.
  5. Plan for using Java EE security from Day One.
  6. Build what you know.
  7. Always use session facades whenever you use EJB components.
  8. Use stateless session beans instead of stateful session beans.
  9. Use container-managed transactions.
  10. Prefer JSPs as your first choice of presentation technology.
  11. When using HttpSessions, store only as much state as you need for the current business transaction and no more.
  12. Take advantage of application server features that do not require your code to be modified.
  13. Play nice within existing environments.
  14. Embrace the qualities of service provided by the application server environment.
  15. Embrace Java EE, don't fake it.
  16. Plan for version updates.
  17. At all points of interest in your code, log your program state using a standard logging framework.
  18. Always clean up after yourself.
  19. Follow rigorous procedures for development and testing.


1. Always use MVC.
Cleanly separate business logic (Java beans and EJB components) from controller logic (servlets/Struts actions) from presentation (JSP, XML/XSLT). Good layering can cover a multitude of sins.

This practice is so central to the successful adoption of Java EE that there is no competition for the #1 slot. Model-View-Controller (MVC) is fundamental to the design of good Java EE applications. It is simply the division of labor of your programs into the following parts:
  1. Those responsible for business logic (the Model -- often implemented using Enterprise JavaBeans™ or plain old Java objects).
  2. Those responsible for presentation of the user interface (the View).
  3. Those responsible for application navigation (the Controller -- usually implemented with Java servlets or associated classes like Struts controllers).
There are a number of excellent reviews of this topic with regard to Java EE; in particular, we direct interested readers to either [Fowler] or [Brown] (see Resources) for comprehensive, in-depth coverage.
There are a number of problems that can emerge from not following basic MVC architecture. The most problems occur from putting too much into the View portion of the architecture. Practices like using JSP tag libraries to perform database access, or performing application flow control within a JSP are relatively common in small-scale applications, but these can cause issues in later development as JSPs become progressively more difficult to maintain and debug.
Likewise, we often see migration of View layer constructs into business logic. For instance, a common problem is to push XML parsing technologies used in the construction of views into the business layer. The business layer should operate on business objects -- not on a particular data representation tied to the view.
However, just having the proper components does not make your application properly layered. It is quite common to find applications that have all three of servlets, JSPs, and EJB components, where the majority of the business logic is done in the servlet layer, or where application navigation is handled in the JSP. You must be rigorous about code review and refactoring to ensure that business logic is handled in the Model layer only, that application navigation is solely the province of the Controller layer, and that your Views are simply concerned with rendering model objects into appropriate HTML and Javascript™.
The value of this recommendation should be even clearer today than in the original version of this article. User interface technologies change rapidly and tying business logic to the user interface makes changes to "just the interface" deeply impact existing systems. Just a few years ago, user interface developers for Web applications could choose from servlets and JSPs, struts, and perhaps XML/XSL transformation. Since then, Tiles and Faces have become popular, and now AJAX is gaining a strong following. It would be a shame to have to redevelop an application's core business logic every time the preferred user interface technology changes.

2. Don't reinvent the wheel.
Use common, proven frameworks like Apache Struts, JavaServer Faces, and Eclipse RCP. Use proven patterns.

Back when we first started helping educate our clients in how to use the then-emerging Java EE standards, we discovered (as did many others) that developing a framework for user-interface development significantly improved developer productivity over building UI applications directly to the base servlet and JSP specifications. As a result, many companies developed their own UI frameworks that simplified the task of interface development.
As open-source frameworks like Apache Struts began to develop [Brown], we believed that the switchover to these new frameworks would be automatic and quick. We thought that the benefits of having an open-source community supporting the framework would be readily apparent to developers, and that they would gain universal acceptance very rapidly -- not only for new development, but in retro-fitted applications as well.
What has proven surprising is that this has turned out to not be the case. We still see many companies maintaining or even developing new user-interface frameworks that are functionally equivalent to Struts or JSF. There are many reasons why this could be true: organizational inertia, "not invented here" syndrome, lack of perceived benefit in changing working code, or possibly even a slight sense of hubris in thinking that you could do things "better" than the open-source developers did in a particular framework.
However, the time is long-past when any of these reasons is worth using as an excuse not to adopt a standard framework. Struts and JSF are not only well accepted in the Java community, but fully supported within the WebSphere runtimes and Rational® tool suites as well. Likewise, in the rich client arena, the Eclipse RCP (Rich Client Platform) has also gained wide acceptance for building standalone rich clients. While not a part of the Java EE standard, these frameworks are now a part of the Java EE community, and should be accepted as such.
Nearly as hubristic as not using an off-the-shelf UI framework is ignoring the lessons captured in [Alur] and [Fowler]. These two books detail the most common reusable patterns that occur in Enterprise Java applications. From simple patterns like session facade (discussed in a later recommendation) to more complex patterns like Fowler's persistence patterns (which have been implemented in many open-source persistence frameworks), these works capture the accumulated wisdom of the Java elders. With apologies to Santayana, those who do not learn from the past are condemned to repeat it -- if they're lucky enough to get the chance after their first failures.

3. Apply automated unit tests and test harnesses at every layer.
Don't just test your GUI. Layered testing makes debugging and maintenance vastly simpler.

There has been quite a shake-up in the methodology world over the past several years as new, lightweight methods that call themselves Agile (such as SCRUM [Schwaber] and Extreme Programming [Beck1] in Resources) become more commonplace. One of the hallmarks of nearly all of these methods is that they advocate the use of automated testing tools to improve programmer productivity by helping developers spend less time regression testing, and to help them avoid bugs caused by inadequate regression testing. In fact, a practice called Test-First Development [Beck2] takes this practice even further by advocating that unit tests be written prior to the development of the actual code itself. However, before you can test your code, you need to isolate it into testable fragments. A "big ball of mud" is hard to test because it does not do a single, easily identifiable function. If each segment of your code does several things, it is hard to test each bit for correctness.
One of the advantages of the MVC architecture (and the Java EE implementation of MVC) is that the componentization of the elements make it possible (in fact, relatively easy) to test your application in pieces. Therefore, you can easily write tests to separately test persistence, session beans, and portions of the user interface outside of the rest of the code base. There are a number of frameworks and tools for Java EE testing that make this process easier. For instance, JUnit, which is an open-source tool developed by junit.org, and Cactus, which is an open source project of the Apache consortium, are both quite useful for testing Java EE components. [Hightower] discusses the use of these tools for Java EE in detail.
Despite all of the great information about deeply testing your application, we still see many projects where it is believed that if the GUI is tested (which may be a Web-based GUI or a standalone Java application), then the entire application has been comprehensively tested. GUI testing is rarely enough. There are several reasons for this.
  1. With GUI testing, it's difficult to test every path through the system; the GUI is only one way of affecting the system. There may be background jobs, scripts, and various other access points that also need to be tested -- but these often don't have GUIs associated with them.
  2. Testing at the GUI level is very coarse-grained. A GUI tests how the system behaves at the macro level of the system, meaning that if problems are found, entire subsystems must be considered, making finding any bugs identified extremely difficult.
  3. GUI testing usually can't be done well until late in the development cycle when the GUI is fully defined. This means that latent bugs won't be found systematically until very late.
  4. Average developers probably don't have access to automatic GUI testing tools. Thus, when a developer makes a change, there is no easy way for that developer to retest the affected subsystem. This actually discourages good testing. If the developer has access to automated code level unit tests, the developer can easily run them to make sure the changes don't break existing function.
  5. If automated builds are done, it is fairly easy to add an automated unit testing suite to the automated build process. By doing this, the system can be rebuilt regularly (often nightly) and regression tested with little human intervention.
In addition, we must emphasize that distributed, component-based development with EJBs and Web services makes testing your individual components absolutely necessary. When there is no GUI to test, you must then fall back on lower-level tests. It is best to start that way, and spare yourself the headache of having to retrofit your process to include those tests when the time comes to expose part of your application as a distributed component or Web service.
In summary, by using automated unit tests, defects are found sooner, defects are easier to find, testing can be made more systematic, and thus, overall quality is improved.

4. Develop to the specifications, not the application server.
Know the specifications by heart and deviate from them only after careful consideration. Just because you can do something doesn't mean you should.

It is very easy to cause yourself grief by trying to play around at the edges of what Java EE anables you to do. We find developers dig themselves into a hole by trying something that they think will work "a little better" than what Java EE allows, only to find that it causes serious problems in performance, or in migration (from vendor to vendor, or more commonly from version to version) later. In fact, this is such an issue with migrations, that [Beaton] calls this principle out as the primary best practice for migration efforts.
There are several places in which not taking the most straightforward approach can definitely cause problems. A common one today is where developers take over Java EE security through the use of JAAS modules rather than relying on built-in spec compliant application server mechanisms for authentication and authorization. Be very wary of going beyond the authentication mechanisms provided by the Java EE specification. This can be a major source of security holes and vendor compatibility problems. Likewise, rely on the authorization mechanisms provided by the servlet and EJB specs, and where you need to go beyond them, make sure you use the spec's APIs (such as getCallerPrincipal()) as the basis for your implementation. This way you will be able to leverage the vendor-provided strong security infrastructure and, where business needs require, support more complex authorization rules. (For more on authorization, see [Ilechko].)
Other common problems include using persistence mechanisms that are not tied into the Java EE spec (making transaction management difficult), relying on inappropriate Java Standard Edition facilities (like threading or singletons) within your Java EE programs, and "rolling your own" solutions for program-to-program communication instead of staying within supported mechanisms like Java 2 Connectors, JMS, or Web services. Such design choices cause no end of difficulty when moving from one Java EE compliant server to another, or even when moving to new versions of the same server. Using elements outside of Java EE often causes subtle portability problems. The only time you should ever deviate from a spec is when there is a clear problem that cannot be addressed within the spec. For instance, scheduling the execution of timed business logic was a problem prior to the introduction of EJB 2.1. In cases like this, we might recommend using vendor-provided solutions where available (such as the Scheduler facility in WebSphere Application Server), or to use third-party tools where these are not available. Today, of course, the EJB specification now provides for time-based function so we encourage the use of standard interfaces. In this way, maintenance and migration to later spec versions becomes the problem of the vendor, and not your own problem.
Finally, be careful about adopting new technologies too early. Overzealously adopting a technology before it has been integrated into the rest of the Java EE specification, or into a vendor's product, is often a recipe for disaster. Support is critical -- if your vendor doesn't directly support a particular technology, you should carefully consider if you should use it. People (particularly developers) tend to focus too much on easing the development process and neglect to consider the long term consequences of depending on large amounts of code developed outside your organization which is not supported by a vendor. We've seen far too many project teams that are enamored with new technology (for example, the latest open source framework) and quickly become dependent upon it without considering the very real costs to the business. Frankly, the decision to use any technology beyond what you've bought from your vendors should be carefully reviewed by corporate architecture, business, and legal teams (or their equivalent in your environment), just as normal product purchasing decisions are evaluated. After all, with rare exceptions, most of us are in the business of solving business problems, not advancing technology for the sheer fun of it.

5. Plan for using Java EE security from Day One.
Turn on WebSphere security. Lock down all your EJBs and URLs to at least all authenticated users. Don't even ask -- just do it.

It's a continual source of astonishment to us how few customers we work with originally plan to turn on WebSphere Application Server's Java EE security. In our estimate, only around 50% of the customers we see initially plan to use this feature. We have even worked with several major financial institutions (banks, brokerages, and so on) that did not plan on turning security on; luckily this situation was usually addressed in review prior to deployment.
Not leveraging Java EE security is a dangerous game. Assuming your application requires security (almost all do), you are betting that your developers can better build a security infrastructure than the one you bought from the Java EE vendor. That's not a good bet. Securing a distributed application is extraordinarily difficult. For example, you need to control access to EJBs using a network-safe encrypted token. In our experience, most home-grown security infrastructures are not secure -- with significant weaknesses that leave production systems terribly vulnerable. (Refer to chapter 18 of [Barcia] for more.)
Reasons cited for not using Java EE security include: fear of performance degradation, belief that other security products like IBM Tivoli® Access Manager and Netegrity SiteMinder handle this already, or ignorance of the features and capabilities of WebSphere Application Server security. Do not fall into these traps. In particular, while products like Tivoli Access Manager provide excellent security features, they alone cannot secure an entire Java EE application. They must work hand in hand with the Java EE application server to secure all aspects of the system.
Another common reason given for not using Java EE security is that the role-based model does not provide sufficiently granular access control to meet complex business rules. Though this is often true, this is no reason to avoid Java EE security. Instead, leverage the Java EE authentication model and Java EE roles in conjunction with your specific extended rules. If a complex business rule is needed to make a security decision, write the code to do it, basing the decision upon the readily available and trustable Java EE authentication information (the user's ID and roles). (For more on authorization, see [Ilechko].)

6. Build what you know.
Iterative development enbales you to gradually master all the moving pieces of Java EE. Build small, vertical slices through your application rather than doing everything at once.

Let's face it, Java EE is big. If a development team is just starting with Java EE, it is far too difficult to try learning it all at once. There are simply too many concepts and APIs to master. The key to success in this environment is to take Java EE on in small, controlled steps.
This approach is best implemented through building small, vertical slices through your application. Once a team has built its confidence by building a simple domain model and back-end persistence mechanism (perhaps using JDBC), and has thoroughly tested that model, they can then move on to mastering front-end development with servlets and JSPs that use that domain model. If a development team finds a need for EJBs, they could likewise start with simple session facades atop container-managed persistence EJBs or JDBC-based DAOs (Data Access Objects) before moving on to more sophisticated constructs like message-driven beans and JMS.
This approach is nothing new, but relatively few teams actually build their skills in this way. Instead, most teams cave in to schedule pressures by trying to build everything at once -- they attack the View layer, the Model layer, and the Controller layer in MVC, simultaneously. Instead, consider adopting some of the new Agile development methods, such as Extreme Programming (XP), that foster this kind of incremental learning and development. There is a procedure often used in XP called ModelFirst [Wiki] that involves building the domain model first as a mechanism for organizing and implementing your user stories. Basically, you build the domain model as part of the first set of user stories you implement, and then build a UI on top of it as a result of implementing later user stories. This fits very well with letting a team learn technologies one at a time, as opposed to sending them to a dozen simultaneous classes (or letting them read a dozen books), which can be overwhelming.
Also, iterative development of each application layer fosters the application of appropriate patterns and best practices. If you begin with the lower layers of your application and apply patterns like Data Access Objects and session facades, you should not end up with domain logic in your JSPs and other View objects.
Finally, when you develop in thin vertical slices, it makes it easier to start early in performance testing your application. Delaying performance testing until the end of an application development cycle is a sure recipe for disaster, as [Joines] relates.

7. Always use session facades whenever you use EJB components.
Use local EJBs when architecturally appropriate.

Using a session facade is one of the best-established best practices for the use of EJBs. In fact, the general practice is widely advocated for any distributed technology, including CORBA, EJB, and DCOM. Basically, the lower the distribution "cross-section" of your application, the less time will be wasted in overhead caused by multiple, repeated network hops for small pieces of data. The way to accomplish this is to create very large-grained "facade" objects that wrap logical subsystems and that can accomplish useful business functions in a single method call. Not only will this reduce network overhead, but within EJBs, it also critically reduces the number of database calls by creating a single transaction context for the entire business function. (This is described in detail in [Brown]. [Alur] has the canonical representation of this pattern, but it is also described in [Fowler] (which generalizes it beyond just EJBs) and in [Marinescu]. See Resources.) The careful reader will realize that this is actually one of the core principles of Service Oriented Architecture (SOA).
EJB local interfaces, introduced as part of the EJB 2.0 specification, provide performance optimization for co-located EJBs. Local interfaces must be explicitly called by your application, requiring code changes and preventing the ability to later distribute the EJB without application changes. If you are certain the EJB call will always be local, take advantage of the optimization of local EJBs. However, the implementation of the session facade itself, typically a stateless session bean, should be designed for remote interfaces. This way, the EJB itself can be used remotely by other clients without major breakage to existing business logic. Since EJBs can have both local and remote interfaces at the same time, this is quite feasible.
For performance optimization, a local interface can be added to the session facade. This takes advantage of the fact that most of the time, in Web applications at least, your EJB client and the EJB will be co-located within the same JVM. Alternatively, Java EE application server configuration optimizations, such as WebSphere "No Local Copies," can be used if the session facade is invoked locally but using the remote interface. However, you must be aware that these alternatives change the semantics of the interaction from pass-by-value to pass-by-reference. This can lead to subtle errors in your code. It is best to use local EJBs, since the behavior is controllable on a bean by bean basis, rather than affecting the entire application server.
If you use a remote interface (as opposed to a local interface) for your session facade, then you may also be able to expose that same session facade as a Web service in a Java EE 1.4 compliant way. (This is because JSR 109, the Web services deployment section of Java EE 1.4, requires you to use the remote interface of a stateless session bean as the interface between an EJB Web service and the EJB implementation.) Doing so is often desirable, since it can increase the number of client types for your business logic.

8. Use stateless session beans instead of stateful session beans.
This makes your system more amenable to failover. Use the HttpSession to store user-specific state.

Stateful session beans are, in our opinion, an idea whose time has come ... and gone. If you think about it, a stateful session bean is exactly the same, architecturally, as a CORBA object -- a single object instance, tied to a single server, which is dependent upon that server for its life. If the server goes down, the object values are lost, and any clients of that bean are thus out of luck.
Java EE application servers providing for stateful session bean failover can workaround some issues, but stateful solutions are not as scalable as stateless ones. For example, in WebSphere Application Server, requests for stateless session beans are load-balanced across all of the members of a cluster where a stateless session bean has been deployed. In contrast, Java EE application servers cannot load-balance requests to stateful beans. This means load may be spread disproportionately across the servers in your cluster. In addition, the use of stateful session beans pushes state to your application server, which is undesirable. Stateful session beans increase system complexity and complicate failure scenarios. One of the key principles of robust distributed systems is the use of stateless behavior whenever possible.
Therefore, we recommend that a stateless session bean approach be chosen for most applications. Any user-specific state necessary for processing should either be passed in as an argument to the EJB methods (and stored outside the EJB through a mechanism like the HttpSession), or be retrieved as part of the EJB transaction from a persistent back-end store (for instance, through the use of Entity beans). Where appropriate, this information can be cached in memory, but beware of the potential challenges that surround keeping the cache consistent in a distributed environment. Caching works best for read-only data.
In general, you should make sure that you plan for scalability from day one. Examine all the assumptions in your design and see if they still hold if your application will run on more than one server. This rule applies not only in application code in the cases outlined above, but also to situations like MBeans and other administrative interfaces.
Avoiding statefulness is not merely an IBM/WebSphere recommendation based on supposed limitations of the IBM tool suite; it is a basic Java EE design principle. See [Jewell] for Tyler Jewell's acerbic opinions on stateful beans, which echo the statements made above.

9. Use container-managed transactions.
Learn how two-phase commit transactions work in Java EE and rely on them rather than developing your own transaction management. The container will almost always be better at transaction optimization.

Using container-managed transactions (CMTs) provides two key advantages that are nearly impossible to obtain without container support: composable units of work, and robust transactional behavior.
If your application code explicitly begins and ends transactions (perhaps using javax.jts.UserTransaction, or even native resource transactions), future requirements to compose modules, perhaps as part of a refactoring, often requires changing the transaction code. For example, if module A begins a database transaction, updates the database and then commits the transaction, and module B does the same, consider what happens when you try to use both from module C. Now, module C, which is performing what is a single logical action, is actually causing two independent transactions to occur. If module B were to fail during an operation, module A's work is still committed. This is not the desired behavior. If, instead, module A and module B both used CMTs, module C can also start a CMT (typically implicitly via the deployment descriptor) and the work in modules A and B will be implicitly part of the same unit of work without any need for complex rework.
If your application needs to access multiple resources as part of the same operation, you need two-phase commit transactions. For example, if a message is removed from a JMS queue and then a record is updated in a database based on that message, it is important that either both operations occur -- or that neither occurs. If the message was removed from the queue and then the system failed without updating the database, this system is inconsistent. Serious customer and business implications result from inconsistent states.
We occasionally see client applications trying to implement their own solutions. Perhaps the application code will try to "undo" the queue operation if the database update fails. We don't recommend this. The implementation is much more complex than you initially think and there are many corner cases (imagine what happens if the application crashes in the middle of this). Instead, use two-phase commit transactions. If you use CMT and access to two-phase commit capable resources (like JMS and most databases) in a single CMT, WebSphere Application Server will take care of the dirty work. It will make sure that the transaction is entirely done or entirely not done, including failure cases such as a system crash, database crash, or whatever. The implementation maintains transactional state in transaction logs. We can't emphasize enough the need to rely on CMT transactions if the application accesses multiple resources. If the resources you are accessing cannot provide for two-phase commit, then of course you have no choice but to use a more complex approach -- but you should do everything as possible within your power to avoid this situation.

10. Prefer JSPs as your first choice of presentation technology.
Use XML/XSLT only in cases where you have multiple presentation output types that must be supported by a single controller and back-end.

There is a common argument that we often hear for why you should choose XML and XSLT as your presentation technology over JSP, and this is that JSP "allows you to mix model and view" too much, and that XML/XSLT is somehow free from this problem. Unfortunately, this is not quite true -- or at least not as black and white as it may seem. XSL and XPath are, in reality, programming languages. In fact, XSL is Turing-complete, even though it may not match most people's definition of a programming language in that it is rules-based and does not have all of the control facilities that programmers may be used to.
The issue is that given this flexibility, developers will take advantage of it. While everyone agrees that JSP makes it easy for developers to do "model-like" behaviors in the view, the fact is that it's possible to do some of the same kinds of things in XSL. While it's very difficult (if not impossible) to do things like calling databases from XSL, we've seen some incredibly complex XSLT stylesheets that perform difficult transformations that still amount to model code.
However, the most basic reason why you should choose JSP as your first option for presentation technology is simply because it's the best supported and best-understood Java EE view technology available. Given the introduction of custom tag libraries, the JSTL, and the JSP 2.0 features, it's become increasingly easy to build JSPs that do not require any Java code, and that cleanly separate model and view. There is significant support (including debugging support) for JSP built into development environments, like IBM Rational Application Developer, and many developers find developing with JSP easier than developing with XSL -- mainly due to how JSP is procedurally based, as opposed to rules-based. While Rational Application Developer supports XSL development, the graphical layout tools and other features supporting JSP (especially when in the context of frameworks like JSF) make it much easier for developers to work in a WYSIWYG way -- something that isn't easily done with XSL.
This is not to say that you should never use XSL, however. There are certain cases where the ability of XSL to take a single representation of a fixed set of data and render it in one of several different ways based on different stylesheets (see [Fowler]) is the best solution for rendering your views. However, this kind of requirement is most often the exception rather than the rule. If you are only ever producing one HTML rendering for each page, then in most cases, XSL is overkill, and it will cause more problems for your developers than it will solve.

11. When using HttpSessions, store only as much state as you need for the current business transaction and no more.
Enable session persistence.

HttpSessions are great for storing information about application state. The API is easy to use and understand. Unfortunately, developers often lose sight of the intent of the HttpSession -- to maintain temporary user state. It's not an arbitrary data cache. We've seen far too many systems that put enormous amounts of data -- megabytes -- in each user's session. Well, if there are 1000 logged-in users, each with a 1 MB HTTP session, that's one gigabyte or more of memory in use just for sessions. Keep those HTTP sessions small. If you don't, your application's performance will suffer. A good rule of thumb is something under 2K-4K. This isn't a hard rule. 8K is still okay, but obviously slower than 2K. Just keep your eye on it and prevent the HttpSession from becoming a dumping ground for data that "might" be used.
One common problem is in using HttpSessions to cache information that can be easily recreated, if necessary. Since sessions are persisted, this is a very expensive decision forcing unnecessary serialization and writing of the data. Instead, use an in memory hash table to cache the data and just keep a key to the data in the session. This enables the data to be recreated should the user fail over to another application server. (See [Brown2] for more.)
Speaking of session persistence, don't forget to enable it. If you don't enable session persistence, should a server be stopped for any reason (a server failure or ordinary maintenance), any user that is currently on that application server will lose their session. That makes for a very unpleasant experience. They have to log in again and redo whatever they were working on. If instead, session persistence is enabled, WebSphere will automatically move the user (and their session) to another application server, transparently. They won't even know it happened. This works so well, that we've actually seen production systems that crash regularly (due to nasty bugs in native code -- not IBM code!) still provide adequate service.

12. Take advantage of application server features that do not require your code to be modified.
With features such as WebSphere Application Server caching and the Prepared Statement cache, the performance gains are substantial and the overhead is minimal.

Best practice #4 above states a clear case as to why you should be very prudent in applying application-server-specific features that modify your code. It makes portability difficult and may make version migration challenging as well. However, there are a suite of application-server specific features, particularly in WebSphere Application Server, that you can and should take full advantage of precisely because they do not modify your code. Your code should be written to the specification, but if you know about these features and how to properly use them you can take advantage of significant performance gains.
For one example of this, in WebSphere Application Server, you should turn on dynamic caching and use servlet caching. The performance gains are substantial and the overhead minimal, while the programming model is unaffected. The merits of caching to improve performance are well understood. Unfortunately, the current Java EE specification does not include a mechanism for servlet/JSP caching. However, WebSphere Application Server provides support for page and fragment caching through its dynamic cache function without requiring any application changes. The cache policy is specified declaratively and configuration is through XML deployment descriptors. Therefore, your application is unaffected, remaining Java EE specification compliant and portable, while benefiting from the performance optimizations provided from WebSphere's servlet and JSP caching.
The performance gains from dynamic caching of servlets and JSPs can be substantial, depending on the application characteristics. Cox and Martin [Cox] showcase performance benefits up to a multiplier of 10 from applying dynamic caching to an existing RDF (Resource Description Format) site summary (RSS) servlet. Please recognize that this experiment involved a simple servlet, and this order of magnitude improvement may not be reflective of a more complex application mix.
For additional performance gains, the WebSphere Application Server servlet/JSP results cache is integrated with the WebSphere plug-in ESI Fragment processor, the IBM HTTP Server Fast Response Cache Accelerator (FRCA) and Edge Server caching capabilities. For heavy read-based workloads, significant additional benefits are gained through leveraging these capabilities. (See performance gains described in [Willenborg] and [Bakalova] in Resources.)
For another example of the principle (which we often observe customers not use simply because they don't know that it exists), take advantage of the WebSphere Prepared Statement Cache when writing JDBC code. By default, whenever you use a JDBC PreparedStatement in WebSphere Application Server, it will compile the statement once and then place it in a cache that will be reused not just later in the same method where the PreparedStatement is created, but across all points in your program where the same SQL code is used in the same or another PreparedStatement. Saving this re-compilation step can result in a significantly lower number of calls to the JDBC driver and improve the performance of your application. You don't have to do anything special to take advantage of this; just write your JDBC code to use PreparedStatements. By writing your code to use a PreparedStatement instead of a regular JDBC Statement class (which uses purely dynamic SQL) you can take advantage of this performance enhancement while not losing any portability.

13. Play nice within existing environments.
Deliver a Java EE EAR and configurable installation scripts, not a black box binary installer.

In most realistic scenarios, large WebSphere Application Server users run multiple applications in the same shared cell. This means that if you provide an application to be installed, it must install reasonably into an existing infrastructure. This means two things: First, you must limit the number of assumptions you make about the environment, and ultimately because you can't possibly anticipate every variant, your installation process must be visible. By visible, we mean that providing a binary executable that is the installer is not acceptable. Administrators performing the installation need to understand what the install process is doing to their cell. To facilitate this, you should deliver an EAR file (or a set of EAR files), along with documentation and installation scripts. The scripts should be readable so that the installer can understand what they do and can validate that there is nothing dangerous being done by the scripts. For situations where the scripts are inappropriate, users may need to install your EARs using some other process that they already use - meaning you must document what your installer is doing!

14. Embrace the qualities of service provided by the application server environment.
Design applications to be clusterable using WebSphere Application Server Network Deployment.

We've already mentioned the importance of leveraging WebSphere Application Server security and transactional support. One more important area that we see ignored far too often is clustering. Applications need to be designed and delivered to run in a clustered environment. Most realistic environments require clustering for scalabity and reliabilty. Applications that don't cluster lead quickly to disaster.
Closely related to clustering is supporting WebSphere Application Server Network Deployment. If you are building an application that you will sell to others, make sure your application runs on WebSphere Application Server Network Deployment and not just the single server versions.

15. Embrace Java EE, don't fake it.
Commit to building real Java EE applications that truly leverage Java EE function.

One of the most disturbing things we've seen more than once is an application that claims to "run in WebSphere" but isn't really a WebSphere application. We've seen several examples where there is a thin piece of code (perhaps a servlet) in WebSphere Application Server and all of the remaining application logic is actually in a separate process; for example, a daemon process written in Java, C, C++ or whatever -- but not using Java EE -- does the real work. That's not a real WebSphere Application Server application. Virtually all of the qualities of service that WebSphere Application Server provides aren't available to such applications. This can be quite a rude awakening for folks that think this is a WebSphere Application Server application.

16. Plan for version updates.
Change is inevitable. Plan for new releases and fix updates so that your customers can stay current.

WebSphere Application Server continues to evolve, and so it should not surprise you that IBM regularly produces fixes for WebSphere Application Server, and that IBM periodically releases new major versions. You need to plan for this. There are two kinds of development organizations that this impacts: in-house developers and third party application vendors. The basic issues are the same, but each is impacted differently.
First, consider fixes. IBM regularly releases recommended updates that fix known bugs in our products. While it is likely impossible to always be running at the latest levels, it is prudent to not fall too far behind. How "far behind" is it okay to be? There is no right answer to this, but you should plan on supporting fix levels within a few months of their release. Yes, this means upgrades in production a few times a year. In-house developers can feel free to skip certain fix levels and support one fix level at a time to reduce testing costs. Application vendors aren't so lucky. If this is you, then you need to support multiple fix levels at the same time so that your customers can run your software in conjunction with other software. If you support only one fix level, it may quite literally be impossible to find fix levels compatible across multiple products. Really, the best approach for vendors is to go with the model of supporting "upwardly compatible fixes." This is the approach IBM uses with regard to support products of other vendors with which we integrate (such as Oracle®, Solaris™, and so on). Refer to our support policy for more information.
Second, consider major version upgrades. Periodically, IBM releases new major releases of our products with major functional upgrades. We continue to support older major releases, but not forever. This means you must plan for forced moves from one major release to another. This is simply unavoidable and must be considered in your cost model. If you are a vendor, this means you have to upgrade your product to support new versions of WebSphere Application Server from time to time, or your customers will be stranded on unsupported IBM products -- which is something we've seen happen more than once! If you are purchasing a product from a vendor, we encourage you to ensure through due diligence that your vendor is committed to supporting new versions of IBM products. Being stranded on unsupported software is a very dangerous situation.

17. At all points of interest in your code, log your program state using a standard logging framework.
This includes exception handlers. Use a logging framework like JDK 1.4 logging or Log4J.

Logging is sometimes the most tedious, undervalued part of programming, but it is the difference between long hours of debugging and going home at a reasonable time. As a general rule of thumb, at every transition point, log it. When you're passing parameters from one method to another method, or between classes, log it. When doing some transformation on an object, log it. When in doubt, log it.
Once you've made the decision to log, choose an appropriate framework. There are lots of good choices out there but we are partial to the JDK 1.4 trace APIs, as they are fully integrated into the WebSphere Application Server trace subsystem and are standards-based.

18. Always clean up after yourself.
If you obtain an object from a pool, always make sure you return it back to the pool.

One of the most common errors we see with Java EE applications, whether running in development, test, or production, are memory leaks. Nine times out of ten, it's because a developer forgot to close a connection (JDBC most of the time) or return an object back into the pool. Make sure that any objects that need to be explicitly closed or returned to the pool are so done. Don't be one of the culprits responsible for the offending code.

19. Follow rigorous procedures for development and testing.
This includes adopting and following a software development methodology.

Large scale system development is hard and it should be taken seriously. Yet, too many times we find teams that are lax in their policies, or that half-heartedly follow development methods that may not apply for the type of development that they are doing, or that they don't understand well. Perhaps the worst extreme of this is trying on the "Development method of the month" where a team will swing from RUP to XP to some other agile method within the lifecycle of a single project.
In short, almost any method will work for most teams provided that they are well-understood by the team members, followed rigorously, and adjusted carefully to deal with the specific natures of the technology and team that is using that method. For teams that have not adopted a method, or have not fully embraced the method that they have chosen, we would refer them to classic works like [Jacobson], [Beck1], or [Cockburn]. Another useful source of information is the recently announced OpenUP plug-in for the Eclipse Process Framework [Eclipse]. And so that we don't repeat too much of what has been said on this topic already, we refer the reader to [Hambrick] and [Beaton2]. (See Resources.)

Conclusion
In this brief summary we have taken you through the core patterns and best practices that can make Java EE development a manageable endeavor. While we have not shown all of the details necessary to put these patterns into practice, we have hopefully given you enough pointers and direction to help you determine where to go next.

Acknowledgements
Thanks to all of those who first documented these patterns and best practices (and whom we reference below), and also to John Martinek, Paul Ilechko, Bill Hines, Dave Artus and Roland Barcia for their help in reviewing this article.

Resources

Kowalski: The Rust-native Agentic AI Framework Evolves to v0.5.0 🦀

  TL;DR: Kowalski v0.5.0 brings deep refactoring, modular architecture, multi-agent orchestration, and robust docs across submodules. If yo...