Remote Procedure Calls (RPC)
This article walks through how remote calls (RPC) are set up in Split Mode and refers to code in the publicly available modular plugin template.
Modules Overview
The plugin is split into three modules: shared, frontend, and backend. The following sections explain how the module dependencies are configured.
Shared Module
The shared module defines the RPC interface. It needs the rpc and kotlinx.serialization plugins.
Frontend Module
The frontend module depends on :shared and needs the rpc plugin as well.
Backend Module
The backend module depends on :shared and requires intellij.platform.kernel.backend and intellij.platform.rpc.backend.
Also declare the backend platform module dependency in backend/src/main/resources/modular.plugin.backend.xml:
Implementing RPC Communication
RPC Interface
Introduce the RPC interface in the shared module.
RPC interface must follow the rules:
The interface must be annotated with
@Rpc.All functions must be
suspend.All parameters and return types must be
@Serializable. They are essentially data transfer objects (DTOs) widely used in client-server application development.Primitives,
String,Flow,Deferredare serializable by default.Enums are not serializable by default. Mark them as
@Serializableexplicitly.Classes must be annotated with
@Serializableand must contain only other serializable fields
For convenience, introduce suspend getInstanceAsync() so the frontend can easily acquire the instance.
RPC Backend Implementation
Add a class implementing the RPC interface in the backend module.
Implement RemoteApiProvider, which registers the RPC implementation with the platform.
Register the provider in the com.intellij.platform.rpc.backend.remoteApiProvider extension point.
backend/src/main/resources/modular.plugin.backend.xml:
Calling RPC from Frontend
ChatRepositoryRpcApi can be called on the frontend:
Note that all getInstanceAsync(), getMessagesFlow(), and sendMessage() functions are suspend. They must be called in some coroutine context.
RPC Error Handling
Calling an RPC API may fail with an RpcClientException, for example, if there are communication problems, service is not ready, etc. For instance, this may happen if the client tries to execute the call while the backend is not fully initialized, if a network problem occurs, or if the backend is restarted while a call is being executed.
Such errors can be mitigated by using the durable wrapper function.
It will retry the call in case discovery of the backend RPC implementation fails.
Serializable Types
All parameters and return types must be @Serializable.
General rules:
Primitives,
String,Flow,Deferredare serializable by default.Enums are not serializable by default and must be annotated with
@Serializable.For types like
LocalDateTime, implement a customKSerializer(see Data Transfer Objects).
Data Transfer Objects
Domain objects are not always directly serializable for transport over RPC.
In the example plugin, ChatMessage is the domain model used on both sides, but it contains LocalDateTime, which is not natively supported by kotlinx.serialization.
The solution is a dedicated DTO class in the shared module, for example, ChatMessageDto:
Custom KSerializer implementations can handle types that are not natively serializable, for example, LocalDateTimeSerializer:
The RPC interface uses the DTO, and each side converts to and from the domain model as needed:
Backend: converts
ChatMessage -> ChatMessageDtobefore emitting viagetMessagesFlow()Frontend: converts
ChatMessageDto -> ChatMessageafter receiving viatoChatMessage()
ID Types
To pass classes commonly used in IntelliJ Platform through RPC, use ID types and helper functions allowing to serialize and deserialize between the full and ID types:
Full Type | ID Type | Serialization | Deserialization |
|---|---|---|---|
|
| ||
|
| ||
|
| ||
|
|
Note that these objects are not fully serializable, so the frontend only receives parts of the backend object. If possible, use only IDs on the frontend and work with the full objects on the backend side.
RPC Examples
Subscribing to the Backend State
BackendChatRepositoryModel holds a MutableStateFlow of messages on the backend:
The BackendChatRepositoryRpcApi RPC implementation simply delegates to this service:
On the frontend, FrontendChatRepositoryModel subscribes to this flow and exposes it as a StateFlow:
In the code above, messagesFlow is initialized with an empty list. Since services are initialized lazily, the first subscriber will trigger the RPC connection, but the state will be emptyList() until the first backend emission arrives.
If this matters for an implemented feature, make sure the service is initialized before its first use — for example, via subscribing to updates from the backend inside a dedicated ProjectActivity.
Pushing Events from Backend to Frontend
To push events from the backend to the frontend without an explicit request, use ApplicationRemoteTopic or ProjectRemoteTopic instead of a regular RPC call.
Example:
Define topic and event in the shared module:
Send events from the backend module:
Handle events in the frontend module:
Register the listener via extension point in
modular.plugin.frontend.xml:
FAQ
What to do with AbstractMethodError?
If an error like this occurs:
Make sure that all the functions in the interface are suspend.
How to efficiently transfer byte[]?
Wrap the data into a fleet.rpc.core.Blob for the sake of reducing the serialization overhead.