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
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");78 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")) > 016 && !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
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}89// CartService (no HTTP types)10public Cart applyDiscount(String id, BigDecimal pct,11 Collection<? extends GrantedAuthority> roles) {12 if (pct.compareTo(MAX_UNAPPROVED_DISCOUNT) > 013 && !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
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,maskandvalidwhenexpress 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.
Related patterns
Usually found in the same codebase.
- .NETHigh
WebForms ViewState and code-behind logic
Maps to SPA front end over a stateless API that rereads authoritative data
- SpringHigh
Spring XML and AOP hidden logic
Maps to Explicit composition with declared cross-cutting concerns
- Java EEMedium
EJB home and remote interfaces
Maps to Coarse-grained REST contract with DTOs
- 4GLMedium
Informix 4GL screen logic
Maps to Request schema plus server-side validation and correction rules
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.