Jakarta Faces Best Practices Checklist
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11) with Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4,
Java 17 or later, Tomcat 11.0.26 with Weld 6.0.4.Final, PrimeFaces 16.0.0 and OmniFaces 5.5.5. Faces 4.0 differences are
named where they matter. It is the last page of the guide proper: a review checklist that points to the page that covers each
rule instead of repeating that page. The section’s landing page and its one-page cheat sheet (both written after this page)
list every page; this one is the page to open before a code review, before a release and before the first deployment.
Everything in a rule below rests on a run recorded on the page it links to (Tomcat 11.0.26, Weld 6.0.4.Final, each Faces
implementation unless a rule says otherwise, 2026-10-08). The page itself added one small check: the Java and XHTML
listings of A view-scoped search bean that loads once and filters in the action (compiled and run on both implementations), An action that registers a loan, queues a message that survives the redirect and answers with a redirect (compiled and run on both implementations) and A form bean whose rules are constraints on the model, read by Faces when the view is validated (compiled and run on both implementations) were compiled with --release 21 against jakarta.faces-api 4.1.2, CDI 4.1.0 and
Jakarta Bean Validation 3.1.1, deployed as an exploded Bookshelf WAR on Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces, and driven with
curl (the Ajax request, the redirect with its flash message, the required and constraint messages). Configuration snippets are copied from the pages that ran them.
How to Use the Checklist
Each item has a bold name, the rule, and a link to the page and section that explains the reason and the observed behaviour. The items are grouped by the question a reviewer asks, and the order of the groups follows the life of a request: design of the beans, rendering, views, input, navigation, Ajax, security, state, configuration, libraries, persistence, servers, testing and deployment. The groups after them collect what not to do:
-
Things to Avoid turns the habits listed as errata in the books of the JSF 1.x and 2.0 era into things to avoid, each with the right form and its page. The habit table of the migration page (Bad Practices the Era Normalised) is the same list seen from a migration; this page is the same list seen from new code.
-
Failures That Print Nothing lists the mistakes that produce no error at all, with the cheap test that finds each.
-
Writing Code That Does Not Depend on One Implementation lists what differs between Mojarra and MyFaces, so that the application does not depend on either.
A checklist is a prompt, not a proof. The last section (Common Mistakes in Using a Checklist) names the ways a checklist is misused.
Design: Scopes and Beans
-
Page state lives in
@ViewScoped. A view-scoped bean (jakarta.faces.view.ViewScoped) lives as long as the user stays on the view and is created again for the next one, which is what a form, a table with a filter or a wizard step needs. The session holds identity and preferences, not the state of pages (View scope, Choosing a scope). -
Know when the view scope ends. Any navigation outcome that builds a new view root ends it, even for the same view id; returning
nullkeeps it. A redirect or a new GET does not destroy the old bean at that moment; it waits for the eviction of the view or the end of the session (When the view scope ends). -
Make every bean in a passivating scope
Serializable. View, session, client-window and conversation beans must be, and so must every@Dependentobject injected into them: Weld refuses a non-serialisable one at deployment (WELD-000072,WELD-001413), and in a cluster or with client state saving it fails at run time otherwise (Serialisation and passivation). -
Use CDI
@Namedbeans only. The native managed beans (@ManagedBeanand thejavax.faces.beanscopes) were removed in Faces 4.0, and a<managed-bean>element infaces-config.xmlis silently ignored on both implementations (Managed beans were removed). -
Let CDI find the bean. A class with only
@Namedis not a bean in the defaultannotateddiscovery mode, and on Tomcat with Weld Servlet nothing is found without aWEB-INF/beans.xml(Bean discovery, beans.xml on Tomcat). -
Keep the bean thin; put rules and queries in a service. The bean translates between the view and the service, the service is a plain CDI bean that can be tested without Faces (Layering, Layers of a Faces application).
-
Use constructor injection correctly. A normal-scoped bean with constructor injection needs a protected no-argument constructor, or deployment fails with
WELD-001435(Constructor injection). -
Inject the Faces objects.
@Inject FacesContext,Flash,ExternalContextand the request-map qualifiers work in Faces 4.x; do not inject them into something created at application start-up, where no request is active (Injecting Faces artefacts). -
Use the scope that matches the interaction. A multi-step process is a flow (
@FlowScoped), a form that must survive several browser tabs independently is@ClientWindowScoped, which needs a client window mode or it fails withWELD-001303(Faces Flows, Multi-Window Support).
package org.example.bookshelf.web;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
import org.example.bookshelf.model.Book;
import org.example.bookshelf.service.BookService;
@Named
@ViewScoped
public class ViewSearchBean implements Serializable {
@Inject
private BookService books;
private String text = "";
private List<Book> results = List.of();
@PostConstruct
void init() {
results = books.search(text);
}
/** Runs once per click, in Invoke Application. */
public void search() {
results = books.search(text);
}
public List<Book> getResults() {
return results;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<title>Search the catalogue</title>
</h:head>
<h:body>
<h:form id="search">
<h:inputText id="text" value="#{viewSearchBean.text}">
<f:validateLength maximum="100"/>
</h:inputText>
<h:message id="textMessage" for="text"/>
<h:commandButton id="go" value="Search" action="#{viewSearchBean.search}">
<f:ajax execute="text" render="results textMessage"/>
</h:commandButton>
<h:panelGroup id="results" layout="block">
<ui:repeat value="#{viewSearchBean.results}" var="book">
<p>#{book.title} by #{book.author}</p>
</ui:repeat>
</h:panelGroup>
</h:form>
</h:body>
</html>
Source: Jakarta Faces 4.1 API: ViewScoped, Jakarta Faces 4.1 VDL: f:ajax and Jakarta EE Tutorial: Using Ajax with Jakarta Faces Technology
On a request with the text dune the Ajax request answered with the two update elements (the message and the results)
and the new view state, and a text of 120 characters came back with the Length is greater than allowable maximum message in
the message element and the list unchanged, on Mojarra 4.1.17 and MyFaces 4.1.4. The bean has no query in a getter: the list
is built in @PostConstruct and in the action.
Rendering: Getters, EL, JSTL and binding
-
A getter returns a field. Faces calls a getter many times in a request: in the performance runs a table of five rows with
renderedexpressions called getters 4 times on a GET and 13 times on a postback on Mojarra (3 and 10 on MyFaces). A query in a getter runs that many times (Getters and EL cost, The cost model). -
Load in
@PostConstruct, in a view action or in the action method. A new model built on every evaluation is evaluated three times before the action and once more to render (Data Tables and Iteration, View actions). -
Do not use JSTL tags as if they ran at render time.
c:ifandc:forEachrun when the view is built; insideui:repeatthey rendered nothing in the runs. Userenderedandui:repeatfor anything that depends on a request-time value (Choosing between JSTL and Facelets tags). -
Bind a component only to a request-scoped bean, or do not bind. A
bindingto a session or application bean shares component instances between requests and users; most uses need no binding at all (The binding attribute). -
Do not name a property
empty.emptyis an EL reserved word, so#{bean.empty}does not parse (Performance pitfalls). -
Treat an empty output as a symptom. An EL name that resolves to nothing renders as empty text with no error, which is the usual symptom of a bean that CDI did not find (A probe for scopes and discovery, Expression language pitfalls).
-
Pick the iteration component on purpose.
h:dataTableandui:repeatdiffer in their attributes and invarStatus;h:dataTablehas none (ui:repeat versus h:dataTable, Mojarra versus MyFaces differences).
Views, Templates and Resources
-
Write one template and fill its slots. Common layout belongs in one template with
ui:insertslots and clients that useui:define; a misspelt slot name is ignored without a message (Templating, its pitfalls). -
Put
f:metadatain a slot that the client defines. Inside a template it is silently ignored (f:metadata). -
Use the Jakarta URNs and an HTML5 doctype. The
jakarta.faces.html,jakarta.faces.core,jakarta.faces.facelets,jakarta.faces.compositeandjakarta.faces.passthroughnamespaces (4.0). The oldjava.sun.comandxmlns.jcp.orgURIs still rendered on 4.1, but their removal is planned for Faces 5.0 (Old namespaces, Namespaces in a migration). -
Repeat markup as a composite component or a tag file, not as copied XHTML. Both take attributes and have their own naming container (Composite Components, Tag Files and Tag Libraries).
-
Reference CSS, scripts and images as resources.
h:outputStylesheet,h:outputScriptand the resource EL give versioning, caching and relocation; a hand-written path gives none of them (Resource tags, Versioning). -
Give an
idto a form and to everything that Ajax or a test refers to. Generated ids change when the view changes (Client ids). -
Label inputs. Use
h:outputLabel forand thelabelattribute of the input, so that messages name the field instead of showing the client id (Accessibility, Messages and labels). -
Do not write JSP or Facelets 1.x. JSP support was removed in Faces 4.0 and the standalone
com.sun.faceletslibrary has been built in since 2.0 (Facelets versus JSP).
Input: Conversion, Validation and Messages
-
Validate at three levels. Faces converters and validators (and the
requiredattribute) reject malformed input on the form, Bean Validation constraints on the model state the rule once, and the service re-checks what it depends on, because a bean is not the only caller of a service (Validators, Bean Validation in the service layer). -
Put the Bean Validation provider on the class path. On Tomcat the constraints do nothing until Hibernate Validator and an EL implementation are in place, and nothing says so (Bean Validation on Tomcat, Activation).
-
Know the empty-value rules. With a provider on the class path an empty optional text field fails
f:validateRegexor a minimumf:validateLengthunlessjakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULListrueor the validators tolerate empty input; a required input missing from the POST is not validated unlessjakarta.faces.ALWAYS_PERFORM_VALIDATION_WHEN_REQUIRED_IS_TRUEis on (Empty values, Empty values and Bean Validation). -
Turn on
f:validateWholeBeandeliberately. It does nothing unlessjakarta.faces.validator.ENABLE_VALIDATE_WHOLE_BEANistrue(Whole-bean validation). -
Keep messages in a bundle. Texts, validator messages and labels come from resource bundles so that they can be translated, and a
validatorMessagereplaces every message of the validators it covers (Messages and Internationalisation, Validator messages). -
Give an entity converter an
equalsmethod. Without it a select list answersValidation Error: Value is not valid(An entity converter, Entity converters). -
Use
immediateonly when you know the phases. An immediate command runs in Apply Request Values and skips validation of the other inputs, which suits a Cancel button and nothing else (Immediate components, Immediate actions). -
Never trust a rendered form. A forged value for a
h:selectOneMenuis rejected by the component, and a button withrendered="false"is not invoked by a forged post; everything else the browser sends is input to validate (Skipping validation, Layers of defence).
package org.example.bookshelf.web;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.io.Serializable;
@Named
@ViewScoped
public class BookForm implements Serializable {
@NotBlank
@Size(max = 200)
private String title;
@Min(1450)
@Max(2100)
private int year = 2000;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
}
A post with an empty title and the year 3000 came back on both implementations with the messages must not be blank and
must be less than or equal to 2100 next to the fields (Hibernate Validator 9.1.4.Final).
Navigation, Post/Redirect/Get and Links
-
Redirect after every POST that changes something. Return an outcome with
?faces-redirect=trueso that reload and Back do not send the form again; carry the message across withFlash(The problem, PRG example). -
Keep messages across the redirect on purpose. A
FacesMessageis lost on a redirect unlessFlash.setKeepMessages(true); the first following request of the session consumes the flash, whichever page and tab it is (Messages across a redirect, Lifetime of a flash entry). -
Use GET for reading, POST for changing.
h:link,h:button,f:viewParamandf:viewActionmake bookmarkable pages; anything that changes state is a command component with a view state (Bookmarkable pages, Navigation by GET). -
Validate view parameters and answer for bad ones. A missing book is a 404, a malformed id a 400; use
responseSendErrortogether withresponseComplete(Not found, Bad request). -
Know that a view action does not run on a postback by default. Set
onPostbackonly when a guard must run on one (onPostback). -
Let the framework build redirect URLs. Use outcomes,
includeViewParamsandf:param; raw or pre-encoded query strings in an outcome were broken on MyFaces (Redirects, Parameters).
package org.example.bookshelf.web;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.Flash;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.bookshelf.service.BookService;
@Named
@RequestScoped
public class BorrowDesk {
@Inject
private FacesContext facesContext;
@Inject
private Flash flash;
@Inject
private BookService books;
private long bookId;
private String member;
/** A POST that changes state answers with a redirect, so reload and Back never repeat it. */
public String borrow() {
int loan = books.borrow(bookId, member);
flash.setKeepMessages(true);
facesContext.addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Loan " + loan + " registered", null));
return "confirmation?faces-redirect=true";
}
public long getBookId() {
return bookId;
}
public void setBookId(long bookId) {
this.bookId = bookId;
}
public String getMember() {
return member;
}
public void setMember(String member) {
this.member = member;
}
}
The POST answered 302 with Location: /bookshelf/confirmation.xhtml, and the confirmation view showed Loan 1 registered
once, on both implementations. MyFaces answered 500 in the earlier runs when the redirecting request had no session yet and a
message was queued; externalContext.getSession(true) before the outcome avoids it
(Mojarra and MyFaces).
Ajax Scoping
-
Execute as little as the request needs and render only what changes. The default
executeoff:ajaxis@this; list the inputs that belong together, render the results and the messages that live next to them, and avoid@alland@formwithout a reason (Execute and render, Ajax scope and cost). -
Render ids that exist. An
idthat is not in the view fails the request; with PrimeFaces an unknownupdateorprocessid is an HTTP 500 for the whole page, and rendering a component withrendered="false"aborts the remaining updates on Mojarra (Render targets, Unknown ids). -
Know the PrimeFaces defaults.
p:commandButtonprocesses@allby default andp:ajaxprocesses@this; they do not matchf:ajax(Process and update). -
Reset input that failed validation when a Cancel or navigation button re-renders it. Use
f:resetValuesorresetValues(Resetting values, f:resetValues). -
List
faces.jsfirst when your script uses it at load time, and use thefaces.ajaxAPI, not hand-writtenXMLHttpRequest(faces.js, The JavaScript API). -
Show errors and a busy state. A
ViewExpiredExceptionreaches the page as a200partial response with an error element; register an error handler and decide what the user sees (Errors, Ajax exceptions). -
Re-render the messages component. Without it a failed validation is invisible (Validation in Ajax requests).
Security
-
Constrain directories and deny the uncovered methods.
/admin/*covered the.xhtmlname, the extensionless name and every path variation tried; a constraint on one file name left the extensionless URL open, and a GET-only constraint left POST open without<deny-uncovered-http-methods/>(Securing URLs in web.xml). -
Secure every mapping of the
FacesServlet. A second mapping such as/faces/*was a second door around a constraint on/admin/*; JoinFaces adds such an alias by default, and a Spring Security rule on/admin/**was bypassed through/faces/admin/...(Securing URLs in web.xml, Spring Security with JoinFaces). -
Leave output escaping on.
h:outputTextand the message components escape by default.escape="false"with user text is XSS; the same applies top:messagesandp:menuitemwithescape="false", to the data ofp:chartandp:timeline, which are written into an inline script, and tob:growlof BootsFaces, which does not escape by default (Cross-site scripting, Chart data and XSS, BootsFaces and security). -
Do not put EL output into JavaScript. EL in a
<script>string or anonclickattribute is HTML-escaped only, which is not JavaScript escaping; pass data indata-*attributes (Cross-site scripting). -
Keep the view state on and server-side for views that change something. Server state is a per-session reference; forged posts with client state or a stateless view were accepted in the runs and ran the action as the victim, so those views need a second defence: protected views, an
Origincheck or tokens (CSRF, Protected views, CSRF beyond the view state). -
Never change state with a GET.
h:link,h:outputLink, a view action that deletes something and an image URL are all GETs that a third-party page can trigger (CSRF beyond the view state, View actions). -
Set the cookie attributes.
HttpOnly,SecureandSameSiteon the session cookie, and cookie tracking only, so that the session id does not appear in URLs (Cookie attributes and SameSite, Session handling). -
Authenticate with Jakarta Security and hash passwords with a configured cost. The session id rotated on login in the runs; the default
Pbkdf2PasswordHashused 2048 iterations, so set the iteration count explicitly (Authentication mechanisms, Identity stores). -
Authorise in the service, not only in the view.
renderedhides a control; roles checked in an interceptor or the service protect the operation (Roles and authorisation). -
Send security headers and a content security policy. Mojarra has its own CSP nonce parameter; PrimeFaces has
primefaces.CSP; a container-level redirect or403carries none of the headers of an application filter (Security headers, Content security policy). -
Treat uploads and downloads as hostile. Limit size, validate name and content type, store under a generated name, escape formula characters in exported CSV and encode the download file name (Validating uploads, CSV formula injection, PrimeFaces file validation).
-
Do not trust forwarding headers for security decisions.
Faces.getRemoteAddr(), which@RateLimitof OmniFaces uses, believesX-Forwarded-For; the rate limit was bypassed by changing the header (OmniFaces CDI annotations). -
Secure the WebSocket channel. A channel and its scope decide who receives a push (Security of push).
-
Do not show internals. Tomcat’s default 500 page shows a stack trace and the server version; map error codes and the exceptions you expect to pages without detail (Error pages, Exception handling and security).
State Saving, Keys and Secrets
-
Keep server state saving unless you can show why not. It is the default, it is the CSRF defence of Security and it keeps state out of the page; client state suits stateless nodes and costs a larger page (Choosing, Client state saving).
-
Configure the client-state keys from the environment, the same on every node. Mojarra reads the JNDI entry
java:comp/env/faces/ClientSideSecretKey(it wasjsf/ClientSideSecretKeybefore 4.0); MyFaces readsorg.apache.myfaces.SECRETandorg.apache.myfaces.MAC_SECRET. With no key the application starts with a random key, and every restart or second node expires the open pages (Keys). -
Do not switch client state encryption off. The unencrypted state is a Java serialisation stream that the server deserialises, and the authentication code goes with the encryption (Auditing client state).
-
Set the PrimeFaces secret too. With JoinFaces
primefaces.SECRETis the public wordprimefacesunlessjoinfaces.primefaces.secretis set from the environment (JoinFaces properties). -
Use stateless views for pages that only read.
<f:view transient="true"/>creates no session and no state on the server, but view-scoped beans become request beans and programmatic changes are lost (Stateless views, View scope and stateless views). -
Make the view state serialisable. An object without a no-argument constructor in component attributes breaks the next postback, and a non-serialisable field breaks replication (Serialisable state).
-
Handle
ViewExpiredExceptionas a normal event. Pages expire (timeout, restart, eviction): an error page or a handler that returns to the view, and a message that says what happened (Expired views, ViewExpiredException). -
Know the view limits. The number of views kept per session differs between the implementations (Mojarra 15, MyFaces 4 sequential views and 20 in total by default), and more tabs than that expire older pages (View limits, Server state limits).
-
Do not use full state saving. It is deprecated for removal in Faces 4.1 and logs a warning; partial state saving is the default (Partial state saving).
-
Keep every secret outside the WAR. Keys, passwords, database URLs and client secrets come from the environment or a secret store; examples and tests use placeholders and a scanner such as
detect-secretsruns in the pipeline (Secrets and configuration).
The Tomcat form of the first rule, with the secrets read from the environment when Tomcat parses the file (the file holds placeholders only):
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- Mojarra -->
<Environment name="faces/ClientSideSecretKey" type="java.lang.String"
value="${FACES_CLIENT_SECRET}" override="false"/>
<!-- MyFaces -->
<Parameter name="org.apache.myfaces.SECRET" value="${MYFACES_SECRET}" override="false"/>
<Parameter name="org.apache.myfaces.MAC_SECRET" value="${MYFACES_MAC_SECRET}" override="false"/>
</Context>
Tomcat substitutes the variables only when started with
-Dorg.apache.tomcat.util.digester.PROPERTY_SOURCE=org.apache.tomcat.util.digester.EnvironmentPropertySource; without it the literal ${...} text becomes the key
and the application fails closed (Keys). The snippet shows both implementations at once; deploy the entry of the
implementation you use.
Configuration and Production
-
Deploy with the
Productionstage. The development stage prints stack traces, component trees and submitted values in error pages; the stage is set withjakarta.faces.PROJECT_STAGEinweb.xml, except on Quarkus, whereweb.xmlis ignored and the stage comes from the build profile, and with JoinFaces, where the default isProduction(Project stages, Development pages, Quarkus development mode). -
Do not rely on a stage for security. The stage changes messages, caching and diagnostics; it does not decide who may do what (What the stage changes).
-
Know where the settings live and in which order they win.
web.xml,faces-config.xml, annotations, and the implementation-specific parameters (com.sun.faces.*,org.apache.myfaces.*) are different families; look up the parameter on the page and in the implementation’s reference (Context parameters, Mojarra parameters, MyFaces parameters). -
Map error codes and exceptions to pages. A catch-all
java.lang.Throwableerror page shadows the specific exception pages; use an error code for the catch-all, and keep the error page itself free of the failing component (Error page pitfalls, The Throwable shadow). -
Cache resources and compress responses. The Production stage sends
max-ageheaders for resources; versioned libraries and a build id in the URL make long caching safe (Caching, Production settings, Resources). -
Measure before tuning. Load test with the same state saving and stage as production (Load testing, Load testing the application).
<context-param>
<param-name>jakarta.faces.PROJECT_STAGE</param-name>
<param-value>Production</param-value>
</context-param>
<session-config>
<cookie-config>
<http-only>true</http-only>
<attribute>
<attribute-name>SameSite</attribute-name>
<attribute-value>Lax</attribute-value>
</attribute>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
<deny-uncovered-http-methods/>
Source: Jakarta Faces 4.1 specification, 7.1.9 ProjectStage Property and Jakarta Servlet 6.1 specification
Add <secure>true</secure> to cookie-config and a CONFIDENTIAL transport guarantee to the security constraints when the application is served over HTTPS,
which in production it is (Cookie attributes and SameSite).
Component Libraries: PrimeFaces, OmniFaces and Others
-
Pin versions that belong together. PrimeFaces 16.0.0 and PrimeFaces Extensions 16.0.0 need Faces 4.0 or later and publish one jar without a classifier; OmniFaces 5 uses the
omnifacesnamespace; match them to the Faces implementation you run (PrimeFaces dependencies, OmniFaces installation, PrimeFaces Extensions setup). -
Ask the six questions before adopting a library. Which Jakarta release does its newest jar target and does it work on 4.1, when was the last release, what does it add to the page, what does it do to security, how much of the server-side API does it override, and how do you leave it (How a library plugs into Faces).
-
Run the three cheap tests on any library. One page with a command component using
action, one Ajax update of a component that renders a<script>, and one message or output component fed with<img src=x onerror=...>; a BootsFaces button silently ignoredactionon Faces 4.1 and its growl ran the handler (Choosing, The ActionSource2 trap). -
Replace libraries that ended. RichFaces, ICEfaces 1.x, Tomahawk, Trinidad, Seam and the Shale project have no release for Jakarta Faces (Discontinued libraries, Replacements).
-
Prefer a component library’s lazy and server-side features for large data. Lazy data models, server-side sorting and paging keep the page small (Lazy data tables).
-
Sanitise instead of switching escaping off. PrimeFaces Extensions has a sanitising converter, but its default decoding of HTML entities turns encoded input into markup; read its page before relying on it (Sanitiser).
-
Do not add a library for what the standard components do. Each library is a dependency to update and a surface to attack (Choosing).
-
Reach for OmniFaces for the gaps. Messages, view parameters, validators across fields, exception handling, resource combining and
o:socketare covered and tested (OmniFaces).
Persistence and Services
-
Open the transaction in the service. A Faces bean is not a transactional boundary, and a runtime exception rolls the transaction back while a checked exception commits unless
rollbackOnsays otherwise (Transactions, Rollback rules). -
Do not hold entities in a view-scoped bean. They fail on the next postback, and a lazy association that is read in Render Response raises
LazyInitializationExceptionthere (Lazy loading, Entities in view scope). -
Choose what each page reads. In the runs a table of 24 books issued 43 statements with plain entities and open-session-in-view, 25 with a join fetch, and 1 with an entity graph or a DTO (Fetching, DTO projections, Entity graphs).
-
Do not make open-session-in-view the design. It turns a mapping mistake in a view into queries that nobody planned (Open session in view).
-
Page in the database. Never load every row and slice in memory; use a lazy model or a paging query (Paging, Paging in the page).
-
Protect concurrent edits. A version column and the exception it raises are a message to the user, not a stack trace (Editing).
Servers and Runtimes
-
Use the Faces implementation of the server, or replace it deliberately. GlassFish 8.0.5 and Payara 7 bundle Mojarra, WildFly 41 bundles Mojarra, Open Liberty bundles MyFaces and TomEE 10.2.0 bundles MyFaces; putting the other implementation in the WAR failed with
WELD-001414(ambiguous bean names) on GlassFish and Payara, with an ambiguous resolution on TomEE and with aNoClassDefFoundErroron Open Liberty’sfaces-4.1feature (thefacesContainer-4.1feature takes Mojarra) (Server matrix, Replacing the implementation). -
Never ship two implementations. A leftover directory with both Mojarra and MyFaces gives
WELD-001414forrequestScope, and the same happens when a test run packages both (Testing pitfalls). -
On Tomcat, match Weld to Faces and add the descriptors. Mojarra 4.1 needs CDI 4.1 (Weld 6.0.x); with Weld 5 it fails with a
ClassNotFoundException, and MyFaces 4.1.4 failed with Weld 7;WEB-INF/beans.xmlis required; the Tomcat servlet, EL and WebSocket classes must not be in the WAR (Versions that fit, Weld start-up, Tomcat and Weld pitfalls). -
With JoinFaces use the
joinfaces.faces.*properties. The oldjoinfaces.jsf.*names are ignored without a message; the application needsserver.servlet.register-default-servlet=truefor aPOST /loginto reach Spring Security, a welcome file forGET /, cookie-only session tracking, andjoinfaces.faces-servlet.url-mappings=*.xhtmlto close the/faces/*alias (Properties, Security, Session). -
With Quarkus put views under
META-INF/resources.src/main/webappis ignored, the context path and the stage are build-time settings, client state needs the secrets from the environment, ando:socketreplacesf:websocket(Layout, State, WebSocket). -
Do not assume another servlet container behaves like Tomcat. Jetty needs its CDI and WebSocket modules and refuses the Tomcat
resource-env-ref(Jetty). -
Keep the Faces and CDI jars
providedon a server that has them. The default WAR of the deployment runs was 22,519 bytes, the Tomcat profile with Faces and Weld 6.1 MB (Packaging).
Testing
-
Test the beans without Faces. Beans and services are plain Java; constructor injection makes them testable (Testing beans, Testing backing beans).
-
Test converters, validators and constraints directly. Faces artefacts have small interfaces, and constraints can be validated with the provider (Converters and validators, Constraints).
-
Test in a container for CDI, injection and the lifecycle. Arquillian deployments or an embedded server give the real wiring; keep the number of such tests small (Arquillian, Container modes).
-
Test the pages with a real browser. Selenium or Playwright, with page objects and waits for Ajax instead of sleeps; HtmlUnit 5.5.0 could not parse the MyFaces
faces.js(Waiting for Ajax, Selenium, Playwright, HtmlUnit). -
Test hostile input and forged requests. A script in a text field, a forged ViewState, a tampered state, a POST of a button that was not rendered (Hostile input, Client state in tests).
-
Run the suite on the stage and the state saving production uses, and on every implementation you support (Project stages in tests, Mojarra and MyFaces).
-
Do not test with JSFUnit. It ended (Where the books are outdated).
-
Use the shared browser-testing page for the parts that are not Faces. Page objects, waits and CI belong to the general guide (End-to-End Testing with Real Browsers).
Deployment and Clustering
-
Build a reproducible WAR and keep the configuration outside it. The Production stage in
web.xml, everything environment-specific in the environment (Packaging, The Production stage). -
Do not run the container as root, and keep credentials out of the image. The official Tomcat image runs Tomcat as root unless told otherwise, which the Dockerfile of the page changes; pass credentials as runtime secrets, never as literals in a Dockerfile (Containers, Orchestrators).
-
Choose sticky sessions or replication on purpose. Sticky sessions are simple and lose state when a node dies; replication needs a distributable application and serialisable state; Quarkus has no session replication, so its instances need sticky sessions with server state or client state with shared keys (Sticky sessions, Session replication, Quarkus state, Failover).
-
Set
com.sun.faces.enableDistributabletotruefor Mojarra in a replicated cluster. Without it the server state did not move with the replicated session and the other node answeredViewExpiredException, even with<distributable/>(Session replication). -
Know that Tomcat replicates a session attribute only when it is set. A session object changed in place after it was stored was not replicated on either implementation (three list additions were lost), and a MyFaces view-scoped bean showed a counter of 1 instead of 4 on the other node (Mojarra with
enableDistributableshowed 4); store an immutable value and callsetAttributeagain, or re-set the attributes in a filter (Changing a session object in place, View scope in a cluster). -
Use the same client-state keys on every node, or stay with server state. Different keys without a shared session expired the page on both implementations (Client state in a cluster).
-
Do not keep per-user data in a node. The flash was lost on another node on Mojarra even with a replicated session, and application-scoped caches, timers and uploads are per node (Node-local state, Push in a cluster).
-
Encrypt the replication channel and secure the proxy. A Tomcat
EncryptInterceptorwith the key from the environment;RemoteIpValveso that the session cookie isSecurebehind TLS termination;ErrorReportValvewithshowReport=falseandshowServerInfo=false(Proxy and TLS). -
Monitor sessions, state and errors. JMX attributes of the session manager, the access log with the request time, and the Faces loggers at the level you need (Monitoring, Logging).
Things to Avoid
The books of the JSF 1.x and 2.0 era, and the forums that grew from them, spread habits that were never good and that migrations carry into new code. They are listed here as things to avoid, each with what happens (from the runs of the linked page) and the form that replaces it. The migration page lists the same habits as a table of old code to remove (Bad Practices the Era Normalised).
| Avoid | What happens | Do instead |
|---|---|---|
Session-scoped beans for page state |
Memory grows with every user and view, and two tabs of one user overwrite each other’s form; the bean is serialised with the session. |
|
Business logic or database queries in getters |
The query ran 4 to 13 times for one table in the performance runs. |
Load in |
|
Component instances are shared between requests and users. |
No |
JSTL |
They run when the view is built; inside |
|
|
The command runs in Apply Request Values; the other inputs are neither validated nor updated, and a listener that calls |
Understand the phases first; use it for Cancel only (Immediate components) |
Secrets, passwords or database URLs in |
Everyone with the repository has them for ever. |
The environment or a secret store (Secrets and configuration, Keys) |
|
Stored or reflected XSS; also |
Escaped output; sanitise when markup must be kept (Cross-site scripting, Sanitiser) |
State-changing |
A third-party page can trigger the change; client state and stateless views were accepted from a forged post. |
POST with view state, protected views, an |
Client-side state saving without encryption and a MAC |
Unencrypted state is a serialisation stream that the server deserialises; the MAC goes with the encryption switch. |
Keep the defaults and set the keys from the environment (Client state saving, Auditing client state) |
Further habits belong to the same family, and the runs of this section show the effect of each:
| Avoid | What happens | Do instead |
|---|---|---|
|
Removed in Faces 4.0; the descriptor entry is ignored and |
CDI |
Entities in view-scoped beans, and open-session-in-view as the design |
Failures on the postback, 43 statements for a table of 24 books. |
DTOs, join fetch or entity graphs (DTO projections, Open session in view) |
A second |
|
Constrain |
Changing a session or view-scoped object in place in a cluster |
Not replicated; the session moved back to a stale copy after failover. |
Immutable values and |
A catch-all |
It shadowed the pages for specific exceptions. |
An error-code page for the catch-all (The Throwable shadow) |
Development stage in production |
Stack traces, component trees and request parameters in error pages; no resource caching. |
|
Hand-written Ajax, Weblets, ajax4jsf or |
|
|
JSP views and |
Removed in Faces 4.0. |
Facelets and Facelets tag libraries (JSP to Facelets, Custom components) |
Adopting RichFaces, Tomahawk or Seam in a new project |
No release for Jakarta Faces. |
PrimeFaces, OmniFaces and the standard components (Replacements) |
Turning off client-state encryption to read the state |
The server deserialises bytes chosen by the client. |
The development stage and server logs (Keys) |
Failures That Print Nothing
These are the mistakes in the runs that gave no error. A checklist item that cannot be seen failing needs a test that does fail; the right-hand column is the cheapest one.
| What happens silently | Cause | How to find it |
|---|---|---|
An EL name renders as empty text |
The bean was not discovered (no |
A page that prints a bean property and a test that opens it (Discovery) |
|
They were removed or renamed |
Search for |
Constraints are not enforced |
No Bean Validation provider on Tomcat |
A test with an invalid value (Bean Validation on Tomcat) |
|
|
A test with an invalid whole bean (Whole-bean validation) |
A misspelt |
Facelets ignores unknown slots; metadata is not read from a template |
A rendered page check (Templating pitfalls, f:metadata) |
A |
No bean-defining annotation under |
A table over the custom model (Custom data models) |
A redirect loses its message |
|
A test that follows the redirect (Messages across a redirect) |
A |
|
A forged postback (onPostback) |
|
The prefix is now |
Read the effective values after start-up (Properties) |
|
The stage is a build-time setting |
Print |
|
Mojarra has its own |
Read the response headers (Content security policy) |
A BootsFaces |
|
A one-line test of every command component (The ActionSource2 trap) |
An unprotected |
The JoinFaces default is a public word |
Read the parameter (Properties) |
Pages expire after every restart |
No client-state key configured |
Restart and post an open page (Keys) |
Replication has no effect |
An object changed in place, or no |
Kill a node (Session replication) |
Writing Code That Does Not Depend on One Implementation
-
Do not depend on view parameters being re-applied on a postback. Mojarra did, MyFaces did not; use a view-scoped bean and
required="#{not facesContext.postback}"(Postback). -
Use
<redirect-param>andincludeViewParamsin outcomes.<redirect include-view-params="true"/>worked on Mojarra only (Include view params). -
Do not inject into classes named in
faces-config.xml. Mojarra left the field null and MyFaces injected it; useCDI.current()(Implementations, Extension points). -
Send plain Java beans, not records, enums or
java.timevalues, throughf:websocket. The two implementations failed or serialised them differently (WebSocket differences). -
Do not depend on the order of
ui:repeatattributessizeandend. Mojarra treatssizeas an end index, MyFaces as a count (ui:repeat attributes). -
Do not depend on the defaults of the view limits, the flash storage or the scope destruction order. They differ (Scopes, Flash).
-
Test with both when the application is meant to run on both. The pages of this section note differences in a closing "Mojarra and MyFaces" section; the list in Mojarra versus MyFaces is the overview.
What Changed
| Version | Change | Effect on the checklist |
|---|---|---|
2.0 |
Facelets built in, |
JSP, hand-written Ajax and resource servlets stopped being needed (Faces 2.0) |
2.2 |
CDI |
The scope and security items of this page (Faces 2.2) |
2.3 |
|
Design: Scopes and Beans and Input: Conversion, Validation and Messages (Faces 2.3) |
3.0 |
|
Every namespace and parameter name (Faces 3.0) |
4.0 |
Native managed beans removed, |
Design: Scopes and Beans, Security, State Saving, Keys and Secrets (Faces 4.0) |
4.1 |
Full state saving deprecated, |
State Saving, Keys and Secrets, Component Libraries: PrimeFaces, OmniFaces and Others (Faces 4.1) |
5.0 (upcoming) |
Old namespaces, |
Code that follows this page compiled against the 5.0.0-M9 milestone without a warning in the migration runs (Preparing for Faces 5.0) |
Where the Books Are Outdated
The eight books consulted for this section teach much that holds: separate view from model, keep beans small, validate on the server, redirect after a post. The parts of their practice that no longer hold:
-
Their examples use JSP, JSP tag handlers, Facelets 1.x,
javax.facesand@ManagedBean, and some keep page state in session beans; Faces 4.1 has none of the first four and a view scope for the last. -
They show hand-written Ajax, Ajax frameworks, RichFaces, Tomahawk, Trinidad, Seam, Shale and JSFUnit; the evidence that these projects ended or have no Jakarta Faces release is in the discontinued libraries section and, for JSFUnit, the testing page.
-
They show client state saving and the state-saving API of Faces 1.x and 2.0, and none of them mentions the key configuration that client state needs today.
-
Their security material relies on the container and on projects that ended (the JSF Security project, Acegi; this statement follows the survey of the books in the security page); Jakarta Security, protected views and client windows came after them, and Security covers the forged-request and escaping cases.
-
None of them covers flows, the CDI view scope, stateless views, CDI injection of Faces objects, WebSocket push, programmatic Facelets, JoinFaces or Quarkus.
Common Mistakes in Using a Checklist
-
Ticking an item you did not test. Every item on this page was observed with a test on a page of the section; copy the test, not only the rule.
-
Testing only the development stage and the default state saving. Several rules (client state keys, the stage, replication) change what is observed.
-
Applying one implementation’s behaviour to the other. The runs differ in the places listed in Writing Code That Does Not Depend on One Implementation.
-
Treating a version number as a rule. The versions here are those verified on 2026-10-08; re-check them before copying (Version matrix).
-
Reading the checklist instead of the page. An item gives the rule; the reason, the lifecycle position and the exceptions are on the page it links.
-
Fixing the symptom. A flash message, a larger buffer or a longer timeout hides the cause that the item names.
Related Pages
-
Migrating to Jakarta Faces — the same habits as old code to remove, and the full list of what the books got outdated.
-
Other Component Libraries — the six questions and three tests for adopting a library.
-
Security, State Saving and Stateless Views, Scopes — the pages behind most items.
-
Testing and Deployment and Clustering — how to check the items and how to run them.
-
Jakarta EE Servers, Tomcat and Weld, Spring Boot with JoinFaces and Quarkus — the runtimes.
-
Persistence and Services — the data layer behind the views.
-
Web UI Frameworks in Spring Boot — when to choose Faces at all.
-
The landing page of the section and its cheat sheet, written after this page, list every page.