Skip to content
PYD ENTERPRISES

Java EE anti-pattern

Struts god Action

One Struts Action class that dispatches on a request parameter to dozens of operations, mixing validation, session state, persistence and business rules inside execute().

FAMILY
Java EE
CUTOVER RISK
High
Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
MODERN MAPPING
Thin controller per command, domain service underneath
GENERATED ARTIFACT
DISCOUNT.dmn.json

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

OrderAction.java (Struts 1.x)Java
1public ActionForward execute(ActionMapping map,
2 ActionForm form, HttpServletRequest req,
3 HttpServletResponse res) throws Exception {
4 OrderForm f = (OrderForm) form;
5 HttpSession s = req.getSession();
6 String cmd = req.getParameter("cmd");
7
8 if ("addLine".equals(cmd)) {
9 Cart c = (Cart) s.getAttribute("cart");
10 c.add(f.getSku(), f.getQty());
11 return map.findForward("edit");
12 } else if ("applyDiscount".equals(cmd)) {
13 Cart c = (Cart) s.getAttribute("cart");
14 BigDecimal pct = new BigDecimal(f.getDiscount());
15 if (pct.compareTo(new BigDecimal("15")) > 0
16 && !req.isUserInRole("MANAGER")) {
17 saveErrors(req, errors("discount.limit"));
18 return map.getInputForward();
19 }
20 c.setDiscount(pct);
21 return map.findForward("edit");
22 } else if ("submit".equals(cmd)) {
23 orderDao.insert((Cart) s.getAttribute("cart"));
24 s.removeAttribute("cart");
25 return map.findForward("confirm");
26 }
27 return map.findForward("edit");
28}

Modern: Thin controller per command, domain service underneath

CartController.javaJava
1@PostMapping("/carts/{id}/discount")
2Cart applyDiscount(@PathVariable String id,
3 @Valid @RequestBody DiscountRequest r,
4 Authentication auth) {
5 return carts.applyDiscount(id, r.percent(),
6 auth.getAuthorities());
7}
8
9// CartService (no HTTP types)
10public Cart applyDiscount(String id, BigDecimal pct,
11 Collection<? extends GrantedAuthority> roles) {
12 if (pct.compareTo(MAX_UNAPPROVED_DISCOUNT) > 0
13 && !hasRole(roles, "MANAGER")) {
14 throw new RuleViolation("discount.limit");
15 }
16 Cart c = repo.get(id);
17 c.setDiscount(pct);
18 return repo.save(c);
19}

Why it is dangerous in modernization

Struts 1 encouraged one Action per screen, and over years each screen's Action absorbed every button on it. The typical result is an execute() method, or a DispatchAction subclass, that branches on a hidden form field such as method or cmd, reads and writes HttpSession attributes, calls DAOs directly, and returns ActionForward names that struts-config.xml maps to JSPs. Validation is split between ActionForm.validate(), validation.xml rules run by the Commons Validator plugin, and ad hoc checks in the Action. Business rules (pricing overrides, approval thresholds) sit between request parsing and the forward. Rewriting the front end in React and calling a new API for each screen loses whatever the Action did that no screen displayed.

How PYD ENTERPRISES detects it

PYD ENTERPRISES parses struts-config.xml (action mappings, form beans, forwards, plug-ins) and validation.xml together with the Java sources, so each Action is linked to its URL path, its ActionForm and its possible forwards. Inside execute(), the dispatch parameter is identified by dataflow from request.getParameter or the form getter to the branch conditions, and each branch is sliced into an operation. The slice is then classified statement by statement: request and session plumbing, persistence calls, and the remaining decisions and calculations, which are business rules. Session attributes read in one operation and written in another are reported as hidden cross-request state.

Maps to: Thin controller per command, domain service underneath

Each dispatch branch becomes one API operation with a typed request body generated from the ActionForm fields it actually uses, and validation rules from validation.xml become JSON Schema constraints. The business logic moves into a domain service that knows nothing about HTTP. Session state that crosses requests becomes either an explicit resource or client state.

Generated specification fragment

DISCOUNT.dmn.jsonJSON
1{
2 "decision": "Discount allowed",
3 "hitPolicy": "FIRST",
4 "inputs": ["discountPercent", "userRole"],
5 "rules": [
6 { "id": "R1", "discountPercent": "<= 15", "userRole": "-",
7 "allowed": true },
8 { "id": "R2", "discountPercent": "> 15",
9 "userRole": "\"MANAGER\"", "allowed": true },
10 { "id": "R3", "discountPercent": "> 15", "userRole": "-",
11 "allowed": false, "message": "discount.limit" }
12 ],
13 "source": "OrderAction.java:15-19"
14}

Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.

Where the rules hide in a Struts application

  • In execute() branches, between parsing the form and choosing a forward, as in the discount check above.
  • In ActionForm.validate(), which runs before execute() when the mapping has validate="true".
  • In validation.xml, where Commons Validator rules such as intRange, mask and validwhen express field constraints declaratively.
  • In JSP scriptlets and custom tags that compute values the user sees but the server never stores.
  • In RequestProcessor subclasses that apply checks to every request.

PYD ENTERPRISES reads all five. The last two are the ones most often missed, because they are not in any Action.

Session state is an undocumented API

The cart in the example lives in HttpSession between requests. addLine writes it, applyDiscount reads and mutates it, submit consumes and removes it. That is a lifecycle with three operations and an implicit resource. The engine lists every session attribute with the operations that create, read, update and remove it, which is the starting point for designing the cart as a real resource with its own identifier and storage, rather than a server-side session that the new stateless API cannot keep.

Struts 2 is not very different

Struts 2 replaced ActionForm with properties on the Action and added interceptors and OGNL, but large Actions with many methods mapped by wildcard or by method attributes in struts.xml show the same pattern. The detector handles both: struts.xml action mappings, interceptor stacks (which can contain business checks) and validation XML files named after the Action class.

Find every Struts god Action in your codebase.

Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.