Skip to content
PYD ENTERPRISES

Java EE anti-pattern

EJB home and remote interfaces

Service boundaries defined by EJBHome and EJBObject interfaces, JNDI lookups and PortableRemoteObject.narrow, where remote calls copy arguments and local calls share them.

FAMILY
Java EE
CUTOVER RISK
Medium
Adds migration effort and contract risk, but behavior is visible in the code once you look.
MODERN MAPPING
Coarse-grained REST contract with DTOs
GENERATED ARTIFACT
openapi.yaml

Before and after

legacy source → modern mapping

Legacy: exhibits the pattern

QuoteClient.javaJava
1public interface QuoteHome extends EJBHome {
2 Quote create(String productCode)
3 throws CreateException, RemoteException;
4}
5
6public interface Quote extends EJBObject {
7 void addDriver(DriverVO d) throws RemoteException;
8 void addVehicle(VehicleVO v) throws RemoteException;
9 PremiumVO rate() throws RemoteException;
10}
11
12// client: four network round trips per quote
13Object ref = ctx.lookup("java:comp/env/ejb/Quote");
14QuoteHome home = (QuoteHome)
15 PortableRemoteObject.narrow(ref, QuoteHome.class);
16Quote q = home.create("AUTO-PA");
17q.addDriver(driver);
18q.addVehicle(vehicle);
19PremiumVO premium = q.rate();

Modern: Coarse-grained REST contract with DTOs

QuoteController.javaJava
1public record QuoteRequest(
2 String productCode,
3 List<Driver> drivers,
4 List<Vehicle> vehicles) { }
5
6public record QuoteResponse(
7 String quoteId,
8 BigDecimal annualPremium,
9 List<RatingFactor> factors) { }
10
11@PostMapping("/quotes")
12QuoteResponse createQuote(@RequestBody QuoteRequest r) {
13 // create + addDriver* + addVehicle* + rate
14 return quoting.rate(r);
15}

Why it is dangerous in modernization

In EJB 2.x a client finds a bean by looking up its home interface in JNDI, narrowing the stub, and calling create or a finder to get a remote interface. Every remote method throws RemoteException, and every argument and return value is serialized: the callee gets a copy. Local interfaces (EJBLocalHome and EJBLocalObject) pass the same objects by reference. Code written against local interfaces can depend on the callee mutating an argument; code against remote interfaces cannot. The interfaces are the closest thing the application has to API contracts, but they are fine-grained, mixed with lifecycle methods, and wrapped in Service Locator and Business Delegate classes that hide which beans call which. Teams that turn every remote method into an endpoint get a chatty API; teams that ignore the pass-by-value versus pass-by-reference distinction change behavior silently.

How PYD ENTERPRISES detects it

The engine resolves each JNDI lookup string through ejb-ref and ejb-local-ref entries and the vendor binding files to a concrete bean, including lookups hidden behind Service Locator caches. Home and component interfaces are linked to the bean class, and each business method is classified as remote or local. For local calls, an escape analysis checks whether the callee writes to fields of its parameters and whether the caller reads them afterwards; those are by-reference dependencies. Call sequences from each client use case are recorded, which shows the coarse operations a new API should expose.

Maps to: Coarse-grained REST contract with DTOs

Group the fine-grained remote calls a use case makes into one operation and publish it as an OpenAPI 3.1 path with request and response schemas generated from the transfer objects. Lifecycle methods (create, remove, finders) become resource operations. Any by-reference dependency becomes an explicit return value in the new contract.

Generated specification fragment

openapi.yamlYAML
1paths:
2 /quotes:
3 post:
4 operationId: createQuote
5 summary: QuoteHome.create, addDriver, addVehicle, rate
6 requestBody:
7 required: true
8 content:
9 application/json:
10 schema:
11 $ref: '#/components/schemas/QuoteRequest'
12 responses:
13 '201':
14 description: Rated quote
15 content:
16 application/json:
17 schema:
18 $ref: '#/components/schemas/QuoteResponse'
19 x-pyd-source: QuoteBean.java, QuoteClient.java:13-19

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.

Stateful session beans hide a conversation

The Quote bean in the example is a stateful session bean: the drivers and vehicles added in earlier calls are held in the bean instance until rate() runs. The interface does not say so; ejb-jar.xml does, with <session-type>Stateful</session-type>. Converting each method to a stateless endpoint loses the accumulated state. PYD ENTERPRISES reads the session type from the descriptor and, for stateful beans, groups the call sequence into one operation, as shown, or into a resource with sub-resources when clients genuinely build the quote over several requests.

Copy semantics are part of the contract

With remote interfaces, addDriver(driver) sends a serialized copy. If the bean normalises the driver's licence number, the client never sees the change. Switch the same call to a local interface during an earlier refactoring and the client's object is modified in place, and some later code may have come to depend on that. The engine reports each method where the callee writes to a parameter, with the interface type in effect, so the new contract can return the normalised value explicitly.

Mapping checklist

  1. Resolve every JNDI name to a bean, including names built from string constants.
  1. Collapse call sequences per use case into coarse operations.
  1. Turn value objects into request and response schemas; drop fields no caller reads.
  1. Map RemoteException and application exceptions to problem details responses with stable error codes.
  1. Keep the transaction attribute of each original method as the transaction boundary of the new operation.

Find every EJB home and remote interfaces 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.