Java EE anti-pattern
EJB 2.x entity beans
Persistent objects whose fields, finder queries, relationships and transaction rules are split between abstract Java classes and ejb-jar.xml, with business logic hiding in container callbacks.
- FAMILY
- Java EE
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- JPA entity, repository and transactional service
- GENERATED ARTIFACT
- Policy.schema.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1public abstract class PolicyBean implements EntityBean {23 public abstract String getPolicyNo();4 public abstract void setPolicyNo(String n);5 public abstract BigDecimal getPremium();6 public abstract void setPremium(BigDecimal p);7 public abstract String getStatus();8 public abstract void setStatus(String s);910 public String ejbCreate(String no, BigDecimal prem)11 throws CreateException {12 if (prem.signum() <= 0) {13 throw new CreateException("E-PREM-01");14 }15 setPolicyNo(no);16 setPremium(prem.setScale(2, RoundingMode.HALF_UP));17 setStatus("PEND"); // default lives here18 return null; // CMP: container sets key19 }2021 public void ejbStore() {22 if ("LAPS".equals(getStatus())) {23 setPremium(BigDecimal.ZERO); // business rule24 }25 }26}
Modern: JPA entity, repository and transactional service
1@Entity2@Table(name = "POLICY")3public class Policy {4 @Id private String policyNo;5 @Column(precision = 11, scale = 2)6 private BigDecimal premium;7 private String status;8}910@Service11public class PolicyService {12 @Transactional // trans-attribute: Required13 public Policy open(String no, BigDecimal prem) {14 if (prem.signum() <= 0) {15 throw new PolicyRuleException("E-PREM-01");16 }17 return repo.save(new Policy(no,18 prem.setScale(2, RoundingMode.HALF_UP), "PEND"));19 }2021 @Transactional22 public void lapse(Policy p) {23 p.setStatus("LAPS");24 p.setPremium(BigDecimal.ZERO); // was ejbStore25 }26}
Why it is dangerous in modernization
An EJB 2.x entity bean is not a plain class. With container-managed persistence (CMP) the bean is abstract: fields exist only as abstract getters and setters, the mapping to tables lives in vendor deployment descriptors, finder methods are EJB QL strings in ejb-jar.xml, and relationships (CMR fields) are declared there too. With bean-managed persistence (BMP) the developer writes JDBC in ejbLoad, ejbStore, ejbCreate and ejbFindByPrimaryKey, and those methods accumulate validation, defaulting and audit logic over the years. Transaction attributes, which decide whether a change commits on its own or with its caller, are also in XML. A port that reads only the Java source sees getters and setters and misses the rules, the queries and the transaction boundaries.
How PYD ENTERPRISES detects it
The Java front end parses source and deployment descriptors together: ejb-jar.xml, and vendor descriptors such as weblogic-cmp-rdbms-jar.xml or ibm-ejb-jar-bnd.xmi, are first-class inputs. Each entity bean becomes a model entity with its CMP fields, CMR relationships with multiplicity and cascade-delete, EJB QL finders parsed into query ASTs, and per-method transaction attributes. For BMP beans, the engine slices ejbLoad, ejbStore and ejbCreate into SQL access, which becomes the table mapping, and everything else, which becomes candidate business rules. Callers are traced through home interfaces so each rule is linked to the use cases that trigger it.
Maps to: JPA entity, repository and transactional service
Fields and relationships become a JPA entity with annotations generated from the descriptor mapping. Finders become Spring Data repository methods or JPQL queries; EJB QL translates closely to JPQL. Logic found in container callbacks moves into a service method with an explicit @Transactional boundary that matches the original transaction attribute, so the commit behavior is the same and is now visible.
Generated specification fragment
1{2 "$schema": "https://json-schema.org/draft/2020-12/schema",3 "title": "Policy",4 "type": "object",5 "required": ["policyNo", "premium", "status"],6 "properties": {7 "policyNo": { "type": "string", "maxLength": 12 },8 "premium": {9 "type": "number", "exclusiveMinimum": 0,10 "multipleOf": 0.01,11 "x-pyd-rule": "E-PREM-01 (PolicyBean.java:12-14)"12 },13 "status": {14 "enum": ["PEND", "ACTV", "LAPS", "CANC"],15 "default": "PEND"16 }17 }18}
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.
Half the bean is XML
The finder that every caller uses to list a customer's active policies is not in PolicyBean.java. It is an EJB QL query in the deployment descriptor, and the transaction attribute that makes premium changes commit with the caller's transaction is next to it.
1<query>2 <query-method>3 <method-name>findActiveByCustomer</method-name>4 <method-params>5 <method-param>java.lang.String</method-param>6 </method-params>7 </query-method>8 <ejb-ql>SELECT OBJECT(p) FROM Policy p9 WHERE p.customerId = ?1 AND p.status = 'ACTV'</ejb-ql>10</query>11...12<container-transaction>13 <method><ejb-name>Policy</ejb-name><method-name>*</method-name></method>14 <trans-attribute>Required</trans-attribute>15</container-transaction>
Rules in ejbStore are time bombs
The container calls ejbStore whenever it synchronises bean state to the database, which is usually at transaction commit but can be earlier depending on the server and its flush settings. A rule written there, like zeroing premium on lapsed policies, runs as a side effect of persistence, not of any business operation. In the rewrite it must become an explicit step of the use case that lapses a policy. PYD ENTERPRISES extracts callback logic as rules with a trigger of the form 'on persist of Policy where status = LAPS', and links them to every code path that can set that status, so reviewers can decide where the rule belongs.
Performance shape changes too
Entity beans accessed through remote interfaces made each getter a network call, and code written around that cost often batches reads in odd ways. Moving to JPA removes the remote hop but adds lazy loading. The generated equivalence tests compare results, not query counts, so teams should still profile the new repository methods against the access patterns PYD ENTERPRISES reports for each finder.
Related patterns
Usually found in the same codebase.
- Java EEMedium
EJB home and remote interfaces
Maps to Coarse-grained REST contract with DTOs
- SpringHigh
Spring XML and AOP hidden logic
Maps to Explicit composition with declared cross-cutting concerns
- PL/SQLCritical
PL/SQL trigger side effects
Maps to Explicit domain operation with events via an outbox
- Java EEHigh
Struts god Action
Maps to Thin controller per command, domain service underneath
Find every EJB 2.x entity beans 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.