Skip to content
PYD ENTERPRISES

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

PolicyBean.java (CMP 2.x)Java
1public abstract class PolicyBean implements EntityBean {
2
3 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);
9
10 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 here
18 return null; // CMP: container sets key
19 }
20
21 public void ejbStore() {
22 if ("LAPS".equals(getStatus())) {
23 setPremium(BigDecimal.ZERO); // business rule
24 }
25 }
26}

Modern: JPA entity, repository and transactional service

Policy.java + PolicyService.javaJava
1@Entity
2@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}
9
10@Service
11public class PolicyService {
12 @Transactional // trans-attribute: Required
13 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 }
20
21 @Transactional
22 public void lapse(Policy p) {
23 p.setStatus("LAPS");
24 p.setPremium(BigDecimal.ZERO); // was ejbStore
25 }
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

Policy.schema.jsonJSON
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.

ejb-jar.xml (excerpt)XML
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 p
9 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.

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.