Implementing a Feature for Split Mode
This page describes a practical flow for implementing new or refactoring an existing feature to make it work natively in Split Mode and behave the same in a monolithic IDE. The steps apply both when migrating an existing plugin and when designing a new one:
Module Structure — Ensure shared, frontend, and backend modules are present.
Code Distribution — Verify that code is properly distributed across shared, frontend, and backend modules.
Data Serialization — Make sure that shared data is serializable.
Data Transfer — Implement RPC for data exchange.
Feature Behavior — Verify correctness in both monolith and split modes.
Optimization — Address issues related to empty state and large state loading.
Tests — Cover the functionality with regular unit tests and integration UI tests running in both monolith and split mode.
1. Identify or Create Necessary Plugin Modules
Refer to the modular plugin template for module structure and necessary dependencies and Modular Plugins for modular plugin concept description.
Create at least those three modules:
<MyPlugin>.Shared– with as few dependencies as possible<MyPlugin>.Backend- withintellij.platform.backenddependency<MyPlugin>.Frontend- withintellij.platform.frontenddependency
Make sure the dependencies are properly described in the build scripts – again, refer to the plugin template.
Put the plugin.xml file into the root plugin module’s resources/META-INF directory.
Put the <MyPlugin>.<ModuleType>.xml file directly into the resources directory in all three freshly created modules.
Reference content modules in the root plugin.xml file.
Expected Outcome
The plugin consists of at least three modules, namely frontend, backend, and shared, plus possibly the existing code that resides in a root plugin module or in other submodules.
2. Put the Existing or Newly Written Code Into Appropriate Module Types
Identify which kind of features dominate in the plugin:
See which APIs it uses more and which type of module they belong to by referring to the list of frontend/backend/shared APIs.
If there are mostly backend extensions in the plugin, consider it backend functionality; otherwise, frontend functionality.
Extract the analyzed functionality into the module type determined during the previous step.
If some APIs are still used in the wrong module type, that’s expected and will be addressed later.
Continue moving the code between modules with a higher level of granularity:
Example: consider a plugin that provides a tool window which receives data to display from an external CLI tool that analyzes a project. Steps to perform:
Move the tool window implementation to the frontend and register it in its XML descriptor.
Extract the code responsible for external process spawning and reading its output to the backend module.
Ensure no more APIs are reported as being used in an inappropriate module type.
Expected Outcome
The plugin is not functioning properly since the UI is now completely detached from the business logic. The code, though, is properly distributed between modules that do not communicate with each other yet. The root plugin module carries only the plugin.xml descriptor in its resources folder. All the code from it is extracted to one of the freshly created modules.
3. Create DTO Classes Required For Data Exchange Between the Frontend UI and Backend Logic
Investigate what information is necessary for the extracted UI components but is known only on the backend side.
Create classes representing the data and annotate them with
@Serializable.Consider using one of the primitive types as a DTO property, or use custom structure with a proper custom serializer implementation (see Data Transfer Objects).
Consider using the serializable form of some major platform primitives if necessary (see ID Types).
Expected Outcome
There are serializable (in terms of kotlinx.serialization framework) DTO classes in the shared module representing the data to be sent over RPC calls.
4. Add the Transport Layer to Connect the UI to the Backend Model
Introduce an RPC interface in the shared plugin module.
The RPC interface must:be annotated with
@Rpcimplement
RemoteApi<Unit>have only
suspendfunctionsuse only
@SerializableDTO classes as function parameters and return type
Introduce RPC implementation in the backend plugin module.
The RPC implementation must:implement the corresponding RPC interface
be registered in the backend module XML descriptor via the
com.intellij.platform.rpc.backend.remoteApiProviderextension point
Use DTOs created in step 3 as input parameters and return values. Get back to step 3 if some data is missing.
Call the RPC where the backend data is required.
It is a crucial detail that RPC calls are always suspending. It may be impossible to use suspending code in a particular place in the frontend functionality, either because it is an old implementation written in Java and is not ready for suspend functions at all, or because the data must be available immediately, otherwise causing poor UX or even freezes. Remember that a proper UX is one of the main reasons we initiated the entire splitting process for, see the split-mode introduction.
RPC can't be called on EDT. Avoid wrapping it in
runBlockingCancellableunless it is absolutely necessary, and you understand all the consequences of such a decision, namely blocking the caller thread and breaking the structured concurrency and suspending API concepts.Consider using the existing platform abstraction for shared state as a reference: FlowWithHistory.kt.
RPC is designed to be initiated by the frontend, which implies that users always interact with one of the IDE UI components that naturally belong to the frontend. In some cases, however, it is required to initiate some UI display from within the backend code. For example, a long backend process may want to show a notification after it finishes. Consider using the Remote Topic API in such cases:
Declare a project- or application-level topic in the shared plugin module by using the
ProjectRemoteTopicorApplicationRemoteTopic, respectivelySubscribe to the topic in the frontend plugin module via the
com.intellij.platform.rpc.applicationRemoteTopicListenerextension point – code to display the desired notification can be invoked herePush the serializable DTO class into the topic in the backend plugin module where necessary – as soon as the DTO is delivered, the frontend topic listener will do its job
Expected Outcome
Frontend UI exchanges serializable data with backend via RPC or RemoteTopic API.
5. Verify and Polish
After all infrastructure has been implemented, it is time to verify the feature behavior and polish it. Refer to Introduction into Split Mode / Remote Development on how to manually test Split Mode and check the monolithic IDE as well – the behavior is expected to be exactly the same.
Expected Outcome
The code is valid from the point of view of this guide, and the feature works as expected in both Split Mode and a monolithic IDE.
6. Review Common Issues
Now that the general functionality works as expected, consider reviewing the list of frequently occurring problems and suggested solutions for them. Necessity of tuning the code depends on the specifics of the feature.
Handle reconnection: wrap RPC calls in
durable {}; this wrapper will restart the call if a network error occurs. Be careful with any side effects your code inside the durable block produces – ideally, avoid them. Example: RecentFileModelSynchronizer.ktRegister actions in proper plugin modules
We strongly encourage registering actions on the frontend side, if possible. The possibility is determined by the action's update method: if it requires backend entities, the action belongs on the backend. Otherwise – on the frontend.
Frontend actions are rendered immediately and do not introduce delays when displaying context menus, popups, or toolbars. If the frontend action decides to touch backend entities in the
AnAction.actionPerformedmethod, it is completely fine to call RPC there.(advanced) In some complicated cases, the
action.update()method might require both frontend and backend entities to be available simultaneously. Such cases must be addressed individually depending on a specific feature description. There are examples of shared, eventually synchronized state implementations in the IntelliJ Platform codebase, for example, in the Recent Files implementation. There, an eventually consistent mutable shared state is implemented to make it possible to access the data model on both the frontend and the backend with no RPC calls required for access.Consider approaching us on the JetBrains Platform Forum to discuss what could be done in your specific case.
Display empty state: UI must render without waiting for backend; show placeholders and progressively fill in data.
Load large state: avoid “send everything at once” RPC implementations; use paging/lazy loading, and request only what the UI needs now. Example: BackendRecentFileEventsModel.kt
Do not load data too frequently: avoid chatty RPC (per keystroke, per scroll tick). Batch requests, cache results, debounce UI events. Example: RecentFilesEditorTypingListener.kt
Expected Outcome
Known issues are mitigated, and the plugin quality is now good enough.
7. Add Tests
Fix the split feature behavior and quality with unit and integration tests if you have not used the TDD approach earlier. See the Running Tests in Split Mode section.
We suggest paying attention to:
Data transformations: correct (de)serialization
Data consistency: correct data merging in case of complicated features with eventually consistent backend/frontend state
Proper behavior under latency: artificial delay in a test implementation backend service does not bring freezes or broken UX
Expected Outcome
The feature implementation has tests covering its correct behavior in remote and local scenarios.
Getting Help
In case of any questions or uncertainties regarding the splitting process, please post a question on the JetBrains Platform Forum. We will try to provide as much help as possible there and reconsider and adjust for unexpected cases.