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:

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 null keeps 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 @Dependent object 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 @Named beans only. The native managed beans (@ManagedBean and the javax.faces.bean scopes) were removed in Faces 4.0, and a <managed-bean> element in faces-config.xml is silently ignored on both implementations (Managed beans were removed).

  • Let CDI find the bean. A class with only @Named is not a bean in the default annotated discovery mode, and on Tomcat with Weld Servlet nothing is found without a WEB-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, ExternalContext and 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 with WELD-001303 (Faces Flows, Multi-Window Support).

A view-scoped search bean that loads once and filters in the action (compiled and run on both implementations)
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;
    }
}
search.xhtml — the view for the bean above; the Ajax request executes only the input and renders the results and its message
<!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>

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 rendered expressions 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:if and c:forEach run when the view is built; inside ui:repeat they rendered nothing in the runs. Use rendered and ui:repeat for 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 binding to 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. empty is 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:dataTable and ui:repeat differ in their attributes and in varStatus; h:dataTable has 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:insert slots and clients that use ui:define; a misspelt slot name is ignored without a message (Templating, its pitfalls).

  • Put f:metadata in 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.composite and jakarta.faces.passthrough namespaces (4.0). The old java.sun.com and xmlns.jcp.org URIs 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:outputScript and the resource EL give versioning, caching and relocation; a hand-written path gives none of them (Resource tags, Versioning).

  • Give an id to 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 for and the label attribute 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.facelets library 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 required attribute) 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:validateRegex or a minimum f:validateLength unless jakarta.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL is true or the validators tolerate empty input; a required input missing from the POST is not validated unless jakarta.faces.ALWAYS_PERFORM_VALIDATION_WHEN_REQUIRED_IS_TRUE is on (Empty values, Empty values and Bean Validation).

  • Turn on f:validateWholeBean deliberately. It does nothing unless jakarta.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN is true (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 validatorMessage replaces every message of the validators it covers (Messages and Internationalisation, Validator messages).

  • Give an entity converter an equals method. Without it a select list answers Validation Error: Value is not valid (An entity converter, Entity converters).

  • Use immediate only 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:selectOneMenu is rejected by the component, and a button with rendered="false" is not invoked by a forged post; everything else the browser sends is input to validate (Skipping validation, Layers of defence).

A form bean whose rules are constraints on the model, read by Faces when the view is validated (compiled and run on both implementations)
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).

  • Redirect after every POST that changes something. Return an outcome with ?faces-redirect=true so that reload and Back do not send the form again; carry the message across with Flash (The problem, PRG example).

  • Keep messages across the redirect on purpose. A FacesMessage is lost on a redirect unless Flash.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:viewParam and f:viewAction make 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 responseSendError together with responseComplete (Not found, Bad request).

  • Know that a view action does not run on a postback by default. Set onPostback only when a guard must run on one (onPostback).

  • Let the framework build redirect URLs. Use outcomes, includeViewParams and f:param; raw or pre-encoded query strings in an outcome were broken on MyFaces (Redirects, Parameters).

An action that registers a loan, queues a message that survives the redirect and answers with a redirect (compiled and run on both implementations)
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 execute of f:ajax is @this; list the inputs that belong together, render the results and the messages that live next to them, and avoid @all and @form without a reason (Execute and render, Ajax scope and cost).

  • Render ids that exist. An id that is not in the view fails the request; with PrimeFaces an unknown update or process id is an HTTP 500 for the whole page, and rendering a component with rendered="false" aborts the remaining updates on Mojarra (Render targets, Unknown ids).

  • Know the PrimeFaces defaults. p:commandButton processes @all by default and p:ajax processes @this; they do not match f:ajax (Process and update).

  • Reset input that failed validation when a Cancel or navigation button re-renders it. Use f:resetValues or resetValues (Resetting values, f:resetValues).

  • List faces.js first when your script uses it at load time, and use the faces.ajax API, not hand-written XMLHttpRequest (faces.js, The JavaScript API).

  • Show errors and a busy state. A ViewExpiredException reaches the page as a 200 partial 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 .xhtml name, 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:outputText and the message components escape by default. escape="false" with user text is XSS; the same applies to p:messages and p:menuitem with escape="false", to the data of p:chart and p:timeline, which are written into an inline script, and to b:growl of 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 an onclick attribute is HTML-escaped only, which is not JavaScript escaping; pass data in data-* 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 Origin check 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, Secure and SameSite on 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 Pbkdf2PasswordHash used 2048 iterations, so set the iteration count explicitly (Authentication mechanisms, Identity stores).

  • Authorise in the service, not only in the view. rendered hides 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 or 403 carries 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 @RateLimit of OmniFaces uses, believes X-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 was jsf/ClientSideSecretKey before 4.0); MyFaces reads org.apache.myfaces.SECRET and org.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.SECRET is the public word primefaces unless joinfaces.primefaces.secret is 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 ViewExpiredException as 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-secrets runs 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):

META-INF/context.xml — the keys come from the process environment
<?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 Production stage. The development stage prints stack traces, component trees and submitted values in error pages; the stage is set with jakarta.faces.PROJECT_STAGE in web.xml, except on Quarkus, where web.xml is ignored and the stage comes from the build profile, and with JoinFaces, where the default is Production (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.Throwable error 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-age headers 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).

web.xml (excerpt) — the production settings that the rules above name; every value is a setting, none is a secret
<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/>

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 omnifaces namespace; 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 ignored action on 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:socket are 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 rollbackOn says 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 LazyInitializationException there (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 a NoClassDefFoundError on Open Liberty’s faces-4.1 feature (the facesContainer-4.1 feature takes Mojarra) (Server matrix, Replacing the implementation).

  • Never ship two implementations. A leftover directory with both Mojarra and MyFaces gives WELD-001414 for requestScope, 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.xml is 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 old joinfaces.jsf.* names are ignored without a message; the application needs server.servlet.register-default-servlet=true for a POST /login to reach Spring Security, a welcome file for GET /, cookie-only session tracking, and joinfaces.faces-servlet.url-mappings=*.xhtml to close the /faces/* alias (Properties, Security, Session).

  • With Quarkus put views under META-INF/resources. src/main/webapp is ignored, the context path and the stage are build-time settings, client state needs the secrets from the environment, and o:socket replaces f: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 provided on 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

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.enableDistributable to true for Mojarra in a replicated cluster. Without it the server state did not move with the replicated session and the other node answered ViewExpiredException, 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 enableDistributable showed 4); store an immutable value and call setAttribute again, 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 EncryptInterceptor with the key from the environment; RemoteIpValve so that the session cookie is Secure behind TLS termination; ErrorReportValve with showReport=false and showServerInfo=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).

Table 1. The errata of the older books as things to avoid
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.

@ViewScoped, @FlowScoped or @ClientWindowScoped (Scopes, Multi-Window Support)

Business logic or database queries in getters

The query ran 4 to 13 times for one table in the performance runs.

Load in @PostConstruct, a view action or the action (Getters, Lifecycle position of a bean)

binding to a session-scoped bean

Component instances are shared between requests and users.

No binding, or a request-scoped bean (The binding attribute)

JSTL c:if and c:forEach used as if they ran at render time

They run when the view is built; inside ui:repeat they rendered nothing.

rendered and ui:repeat (JSTL or Facelets)

immediate="true" as a way to skip validation

The command runs in Apply Request Values; the other inputs are neither validated nor updated, and a listener that calls renderResponse() skips the model update.

Understand the phases first; use it for Cancel only (Immediate components)

Secrets, passwords or database URLs in faces-config.xml, web.xml or beans

Everyone with the repository has them for ever.

The environment or a secret store (Secrets and configuration, Keys)

escape="false" on user-supplied content

Stored or reflected XSS; also p:messages, p:menuitem, b:growl and chart data.

Escaped output; sanitise when markup must be kept (Cross-site scripting, Sanitiser)

State-changing h:link or GET, or turning off the view state or CSRF protection

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 Origin check (CSRF, Protected views)

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:

Table 2. More things to avoid
Avoid What happens Do instead

@ManagedBean, @ManagedProperty from javax.faces.bean, <managed-bean>

Removed in Faces 4.0; the descriptor entry is ignored and @ManagedProperty without @Inject leaves a null field.

CDI @Named and @Inject (Managed beans were removed, ManagedProperty)

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 FacesServlet mapping that nobody secured

/faces/admin/panel.xhtml bypassed the constraint on /admin/*.

Constrain /*, or each prefix, or map nothing else (Securing URLs in web.xml)

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 setAttribute (In-place changes)

A catch-all java.lang.Throwable error page

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.

Production (Project stages)

Hand-written Ajax, Weblets, ajax4jsf or jsf.js

jsf.js is a missing resource in Faces 4.x; the others ended.

f:ajax and faces.js (faces.js, jsf.js to faces.js)

JSP views and UIComponentELTag custom components

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.

Table 3. Silent failures, observed on the linked page
What happens silently Cause How to find it

An EL name renders as empty text

The bean was not discovered (no beans.xml on Tomcat, @Named without a scope in annotated mode)

A page that prints a bean property and a test that opens it (Discovery)

<managed-bean> and the javax.faces.* parameters have no effect

They were removed or renamed

Search for javax.faces in every file type (Symptoms of a half migration)

Constraints are not enforced

No Bean Validation provider on Tomcat

A test with an invalid value (Bean Validation on Tomcat)

f:validateWholeBean does nothing

ENABLE_VALIDATE_WHOLE_BEAN is not true

A test with an invalid whole bean (Whole-bean validation)

A misspelt ui:define or an f:metadata inside a template is ignored

Facelets ignores unknown slots; metadata is not read from a template

A rendered page check (Templating pitfalls, f:metadata)

A @FacesDataModel class is replaced by a scalar wrapper

No bean-defining annotation under annotated discovery

A table over the custom model (Custom data models)

A redirect loses its message

setKeepMessages(true) is missing

A test that follows the redirect (Messages across a redirect)

A f:viewAction guard does not run on a postback

onPostback defaults to false

A forged postback (onPostback)

joinfaces.jsf.* properties are ignored

The prefix is now joinfaces.faces.*

Read the effective values after start-up (Properties)

jakarta.faces.PROJECT_STAGE in web.xml has no effect on Quarkus

The stage is a build-time setting

Print facesContext.getApplication().getProjectStage() (Development mode)

jakarta.faces.ENABLE_CSP_NONCE has no effect on Mojarra 4.1.17

Mojarra has its own com.sun.faces.enableCspNonce

Read the response headers (Content security policy)

A BootsFaces b:commandButton never invokes action

UICommand no longer implements ActionSource2 in Faces 4.1

A one-line test of every command component (The ActionSource2 trap)

An unprotected primefaces.SECRET

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 enableDistributable on Mojarra

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> and includeViewParams in 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; use CDI.current() (Implementations, Extension points).

  • Send plain Java beans, not records, enums or java.time values, through f:websocket. The two implementations failed or serialised them differently (WebSocket differences).

  • Do not depend on the order of ui:repeat attributes size and end. Mojarra treats size as 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

Table 4. Practice changes by version
Version Change Effect on the checklist

2.0

Facelets built in, f:ajax, resources, system events

JSP, hand-written Ajax and resource servlets stopped being needed (Faces 2.0)

2.2

CDI @ViewScoped, flows, stateless views, ClientWindow, protected views

The scope and security items of this page (Faces 2.2)

2.3

f:websocket, injection of Faces artefacts, managed converters and validators, f:validateWholeBean

Design: Scopes and Beans and Input: Conversion, Validation and Messages (Faces 2.3)

3.0

javax became jakarta

Every namespace and parameter name (Faces 3.0)

4.0

Native managed beans removed, jsf.js became faces.js, new URNs, @ClientWindowScoped, SameSite cookie attributes, the JNDI key renamed to faces/ClientSideSecretKey

Design: Scopes and Beans, Security, State Saving, Keys and Secrets (Faces 4.0)

4.1

Full state saving deprecated, UICommand without ActionSource2, injectable Flow

State Saving, Keys and Secrets, Component Libraries: PrimeFaces, OmniFaces and Others (Faces 4.1)

5.0 (upcoming)

Old namespaces, @FacesConfig version, ActionSource2 and other deprecated API planned for removal

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.faces and @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.

The migration page keeps the complete list, area by area, book by book and page by page (by area, by book, by page).

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.