Remote Method Invocation (RMI) was once the straightforward way for Java apps to talk across a network. But it’s tightly coupled to the JVM, brittle across NAT and firewalls, and awkward to expose on the public internet. Modern systems prefer WebSockets: a simple, bidirectional, cross-platform channel that works well from browsers, mobile apps, and servers alike (via RFC 6455). This guide walks you through how to migrate from RMI to WebSockets without rewriting your entire system at once. We’ll keep the tone practical, use small, realistic code slices, and point to battle-tested docs along the way.
Why move off RMI?
- Connectivity and firewalls. RMI often struggles behind NAT and corporate firewalls and needs custom socket factories or extra plumbing; callbacks are especially painful. WebSockets ride over HTTP(S) and upgrade to a persistent TCP connection, which tends to pass through proxies and load balancers much more reliably.
- Interoperability. RMI ties you to Java. WebSockets are language-agnostic: browsers, Node.js, Python, Go, Java—everything speaks it. With subprotocols like STOMP, you can add routing and pub/sub semantics.
- Scalability and operations. Long-lived, full-duplex connections with less overhead than HTTP long polling. Modern proxies and cloud load balancers support WebSockets out of the box.
Migration strategy in three phases
The safest path is incremental. You don’t “flip a switch”; you strangle the RMI endpoints behind a WebSocket façade and move features gradually.
Phase 1 — Introduce a WebSocket edge in front of RMI
Keep your RMI services running, but expose a WebSocket gateway that translates JSON messages into RMI calls. New clients (or the parts of your app you’re actively modernizing) connect via WebSocket; the gateway invokes legacy RMI under the hood.
[ Browser / Mobile / New Services ] <--WebSocket--> [ Gateway ] <--RMI--> [ Legacy Services ]
Benefits: no risky big-bang rewrite; you can test path by path.
Phase 2 — Carve out services
As you modernize internals, replace RMI implementations with native WebSocket handlers (or message-brokered services) one by one. The gateway keeps the protocol stable while you swap the backend.
Phase 3 — Retire RMI
Once traffic is fully moved, decommission the RMI registry and stubs. Remove compatibility layers and simplify message models.
Designing the message API (RMI method ⇒ WebSocket message
RMI exposes strongly typed Java interfaces; WebSocket sends text (often JSON) or binary frames. The trick is to codify your methods as messages:
RMI interface (legacy):
public interface OrderService extends Remote {
Order submit(OrderRequest req) throws RemoteException;
Order get(String orderId) throws RemoteException;
}
WebSocket message shapes (new):
// Request (client -> server)
{ "type": "Order.Submit", "payload": { "sku": "ABC-123", "qty": 2 } }
// Response (server -> client)
{ "type": "Order.Submitted", "payload": { "id": "o-987", "status": "PENDING" } }
// Query
{ "type": "Order.Get", "payload": { "id": "o-987" } }
// Result
{ "type": "Order.Snapshot", "payload": { "id": "o-987", "status": "PENDING" } }
You can evolve this into a small schema: type, correlationId, payload, and optional errors.
A minimal WebSocket server in Java (Jakarta/JSR-356)
JSR-356 is the standard Java API for WebSockets and is supported in modern Java servers (Tomcat, Jetty, Undertow, etc.). Here’s a dead-simple endpoint you can drop into a servlet container.
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import com.fasterxml.jackson.databind.ObjectMapper;
@ServerEndpoint("/ws")
public class GatewayEndpoint {
private static final ObjectMapper MAPPER = new ObjectMapper();
@OnOpen
public void onOpen(Session session) {
// Could auth here (cookies/JWT), start heartbeats, etc.
}
@OnMessage
public void onMessage(String raw, Session session) throws Exception {
Message msg = MAPPER.readValue(raw, Message.class);
switch (msg.type()) {
case "Order.Submit" -> {
OrderRequest req = MAPPER.convertValue(msg.payload(), OrderRequest.class);
// bridge to legacy RMI:
OrderService rmi = RmiLocator.lookup();
Order order = rmi.submit(req);
send(session, new Message("Order.Submitted", order, msg.correlationId()));
}
case "Order.Get" -> {
OrderService rmi = RmiLocator.lookup();
Order order = rmi.get((String) msg.payload().get("id"));
send(session, new Message("Order.Snapshot", order, msg.correlationId()));
}
default -> send(session, Message.error("UnknownType", msg.correlationId()));
}
}
@OnClose
public void onClose(Session session, CloseReason reason) { }
private void send(Session s, Message m) throws Exception {
s.getBasicRemote().sendText(MAPPER.writeValueAsString(m));
}
}
If you’re already on Spring, its WebSocket+STOMP support gives you topic routing, @MessageMapping handlers, and a built-in broker—handy when transitioning from RPC-style calls to events.
A minimal browser client
<script>
const ws = new WebSocket("wss://example.com/ws");
const pending = new Map();
ws.onopen = () => {
call("Order.Submit", { sku: "ABC-123", qty: 2 })
.then(console.log)
.catch(console.error);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.correlationId && pending.has(msg.correlationId)) {
const { resolve, reject } = pending.get(msg.correlationId);
pending.delete(msg.correlationId);
msg.error ? reject(msg) : resolve(msg.payload);
} else {
// unsolicited push: status updates, notifications, etc.
console.log("event", msg);
}
};
function call(type, payload) {
const correlationId = crypto.randomUUID();
ws.send(JSON.stringify({ type, payload, correlationId }));
return new Promise((resolve, reject) => pending.set(correlationId, { resolve, reject }));
}
</script>
Handling authentication, sessions, and security
- TLS everywhere: use
wss://and terminate TLS at your reverse proxy or cloud load balancer. WebSockets upgrade from HTTP(S), so your existing cert story applies. Modern load balancers like AWS ALB support WebSockets natively. - Auth tokens: reuse your HTTP auth (cookies or JWT). Send JWT in the initial HTTP
Upgraderequest (cookie orAuthorizationheader) and validate during@OnOpen. Spring also has first-class integration when using STOMP + Spring Security. - Cross-origin: apply CORS-like checks before accepting a session; for browsers, origin checking still matters even with WebSockets.
Operating WebSockets in production (proxies, timeouts, scaling)
Reverse proxy / LB. NGINX and many others support WebSockets; ensure you forward Upgrade and Connection headers and configure timeouts to avoid idle disconnects. With NGINX, be explicit about proxy timeouts; don’t confuse connect timeout (handshake) with read timeout (idle).
Cloud LBs. AWS Application Load Balancer supports WebSockets and uses the HTTP/1.1 upgrade path; use target groups just like HTTP services.
Sticky sessions? If your server relies on in-memory session state, use session affinity or move state to a shared store so any instance can resume a connection after reconnect.
Backpressure. Browsers don’t give you perfect receive-side backpressure; plan for bufferedAmount, server-side rate limiting, and flow control semantics in your protocol (acks, window sizes). Reactive frameworks help when you own both ends.
Mapping exceptions and versioning
RMI propagates checked exceptions across the wire. Over WebSockets, normalize errors:
{ "type": "Error", "correlationId": "123", "payload": { "code": "Order.NotFound", "message": "No order o-987" } }
Introduce v (version) in your messages early:{ "type": "Order.Submit", "v": 2, "payload": { ... } }
This lets you evolve payloads without breaking old clients.
Performance notes (what to expect)
- WebSockets avoid the request/response churn of long polling and typically reduce latency and server CPU at scale. Expect fewer context switches and lower header overhead per message.
- Batch small updates, prefer binary frames for large payloads, and compress carefully (permessage-deflate) to avoid CPU spikes.
Testing the migration
- Contract tests for each message type (round-trip JSON ↔ DTO) to ensure you didn’t change payload shapes inadvertently.
- Soak tests through the LB to validate idle timeouts and reconnection behavior.
- Chaos tests: drop connections to confirm idempotency of retried messages (your gateway should handle “at-least-once” semantics cleanly).
Example: Bridging a single RMI call
RMI lookup utility in the gateway:
import java.rmi.Naming;
public final class RmiLocator {
private RmiLocator() {}
public static OrderService lookup() {
try {
// rmi://host:1099/OrderService
return (OrderService) Naming.lookup(System.getenv("RMI_URL"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Converting DTOs:
- Keep the legacy DTOs, but annotate a Web DTO that mirrors fields and is stable across clients. Use Jackson to map between them; this lets you slowly reshape the public API while the RMI types stay untouched.
Example: Moving from RPC to events (with Spring + STOMP)
If your RMI callbacks pushed updates, map them to topics:
Server:
@Controller
public class OrderController {
@MessageMapping("/order.submit")
@SendTo("/topic/orders")
public OrderSubmitted submit(OrderRequest request) {
// call legacy or new service
return new OrderSubmitted(/*...*/);
}
}
Client (browser):
const sock = new SockJS("/ws-endpoint");
const stomp = Stomp.over(sock);
stomp.connect({}, () => {
stomp.subscribe("/topic/orders", (msg) => console.log(JSON.parse(msg.body)));
stomp.send("/app/order.submit", {}, JSON.stringify({ sku: "ABC-123", qty: 2 }));
});
Spring’s guide and reference explain STOMP frames, destinations, and message converters in detail.
Cutover checklist
- WebSocket gateway deployed behind a proxy/LB that supports
Upgrade. - Health checks and metrics: connection counts, sends/receives per second, average
bufferedAmount, reconnect rates. - Heartbeats/pings to detect dead peers; configure proxy/LB idle timeouts accordingly.
- Error model and versioning documented.
- Rollback: clients can still speak RMI (or your gateway still routes to RMI) during the early phases.
Common pitfalls (and fixes)
- 502/101 errors through chained proxies. Ensure both hops preserve
Connection: UpgradeandUpgrade: websocket. Validate withcurl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" .... Double proxies are a classic gotcha. Server Fault - Serialization differences. Spring’s
@MessageMappingdoesn’t always reuse your MVC Jackson config; wire a sharedObjectMapperor configure message converters for WS specifically. Stack Overflow - NAT traversal assumptions. One reason RMI was painful was callbacks through NAT/firewalls; WebSockets help because the client initiates and keeps the TCP connection open. Still, plan for reconnects and stateless handlers. Oracle Forums
Closing thoughts
The jump from Java-only RPC (RMI) to open, event-friendly WebSockets is less scary when you start with a gateway, freeze your message schema early, and iterate. You’ll gain simpler networking, better interoperability, and a cleaner path to web and mobile clients.
Useful links (deep dives & docs)
- Jakarta WebSocket / JSR-356 overview and examples — Oracle & Vonage tutorials. OracleVonage API Developer
- Spring WebSocket + STOMP guide and reference — official docs and sample repo. HomeHomeGitHub
- WebSockets vs long polling — practical comparisons and trade-offs. Ably RealtimeMediumGeeksforGeeks
- NGINX as a WebSocket reverse proxy — config patterns and gotchas. F5, Inc.Stack Overflow
- AWS ALB WebSocket support — using upgrade with HTTP/HTTPS listeners. AWS Documentation
- RMI + NAT/firewall background — why callbacks were hard. Oracle Forums+1airccse.org
- Backpressure and flow control — design considerations for realtime streams. Stack OverflowMedium+1
Thank you!
We will contact you soon.
Eleftheria DrosopoulouSeptember 4th, 2025Last Updated: August 26th, 2025

This site uses Akismet to reduce spam. Learn how your comment data is processed.