diff --git a/articles/_vaadin-version.adoc b/articles/_vaadin-version.adoc
index c0f4c7a0b2..f9fb8a8f53 100644
--- a/articles/_vaadin-version.adoc
+++ b/articles/_vaadin-version.adoc
@@ -5,4 +5,4 @@
:vaadin-seven-version: 7.7.49
:vaadin-eight-version: 8.28.4
:spring-boot-version: 4.0.6
-:swing-bridge-version: 1.1.2
+:swing-bridge-version: 1.2.0
diff --git a/articles/tools/modernization-toolkit/swing-bridge/interop/calling-swing-methods.adoc b/articles/tools/modernization-toolkit/swing-bridge/interop/calling-swing-methods.adoc
new file mode 100644
index 0000000000..e1d8b1f420
--- /dev/null
+++ b/articles/tools/modernization-toolkit/swing-bridge/interop/calling-swing-methods.adoc
@@ -0,0 +1,396 @@
+---
+title: Calling Swing Methods
+page-title: Call Swing Methods from Vaadin | SwingBridge
+description: Expose Swing methods with @ExposedMethod and call them from Vaadin code through a typed bridge.
+meta-description: Expose Swing methods with @ExposedMethod, choose between sync and async invocation, and call them from Vaadin views through a typed BridgeHandle.
+order: 1
+---
+
+include::{articles}/_vaadin-version.adoc[]
+
+[[swing-bridge.interop.calling-swing-methods]]
+= Calling Swing Methods from Vaadin
+
+A Vaadin view calls a Swing method by going through a generated bridge interface. The Swing side declares which methods are callable with `@ExposedMethod`; the build's Maven plugin generates a typed `*Bridge` interface plus a proxy that handles EDT dispatch; the Vaadin side obtains a handle through `SwingBridge.interop().of(MyBridge.class)`.
+
+
+[[swing-bridge.interop.calling-swing-methods.swing-side]]
+== Exposing a Method on the Swing Side
+
+Annotate any `public` method on a Swing class with `@ExposedMethod`:
+
+[source,java]
+----
+package com.example.swingapp;
+
+import com.vaadin.swingbridge.interop.ExposedMethod;
+import javax.swing.JFrame;
+
+public class MainWindow extends JFrame {
+
+ @ExposedMethod
+ public void hideNavigation() {
+ navigationPanel.setVisible(false);
+ revalidate();
+ }
+
+ @ExposedMethod
+ public int countSelectedItems() {
+ return itemList.getSelectedIndices().length;
+ }
+}
+----
+
+At build time, the codegen plugin emits a matching bridge interface:
+
+[source,java]
+----
+// Generated into target/generated-sources/swing-bridge/
+package com.example.swingapp;
+
+public interface MainWindowBridge {
+ void hideNavigation();
+ int countSelectedItems();
+}
+----
+
+The Swing source remains a normal Swing class. It doesn't import any Vaadin types.
+
+
+[[swing-bridge.interop.calling-swing-methods.invocation-shapes]]
+== The Four Invocation Shapes
+
+`@ExposedMethod` takes one optional attribute, `invocation()`, with values `Invocation.SYNC` (default) and `Invocation.ASYNC`. Combined with the method's return type, four useful shapes emerge:
+
+[cols="2,2,3,4", options="header"]
+|===
+| Return type | `invocation()` | Bridge method shape | When to use
+
+| `void`
+| `SYNC` (default)
+| `void`, fire-and-forget
+| Stateless commands. The Vaadin thread returns immediately; the EDT runs the work later.
+
+| `T` (non-`void`)
+| `SYNC`
+| `T`, blocks the Vaadin caller until the EDT replies (default timeout: 10 s)
+| Quick state queries that won't show a dialog or take long on the EDT.
+
+| `T`
+| `ASYNC`
+| `CompletableFuture`, non-blocking
+| Anything that might show a dialog, run slowly on the EDT, or otherwise risk blocking the Vaadin UI thread.
+
+| `CompletableFuture` (explicit)
+| `SYNC`
+| `CompletableFuture`, forwarded as-is
+| The Swing developer wants to compose multiple async stages themselves.
+|===
+
+Codegen rejects two combinations:
+
+- `@ExposedMethod(invocation = ASYNC)` on a method whose return type is already `CompletableFuture` (redundant).
+- `@ExposedMethod(invocation = ASYNC)` on a method whose return type is `Runnable` (incompatible with the adder/remover-style listener pattern — see <>).
+
+
+[[swing-bridge.interop.calling-swing-methods.invocation-shapes.examples]]
+=== Examples for Each Shape
+
+*Void, fire-and-forget* — typical for UI commands:
+
+[source,java]
+----
+@ExposedMethod
+public void refreshSidebar() {
+ sidebar.reload();
+}
+----
+
+*Synchronous return* — for cheap queries that complete on the EDT immediately:
+
+[source,java]
+----
+@ExposedMethod
+public String getCurrentLocale() {
+ return localeService.getCurrentTag();
+}
+----
+
+*Async return (codegen wraps the future)* — for anything that might block on the EDT:
+
+[source,java]
+----
+@ExposedMethod(invocation = Invocation.ASYNC)
+public Boolean confirmLeaveCurrentEditor() {
+ // Shows a modal Swing dialog. Don't block the Vaadin thread for this.
+ return JOptionPane.showConfirmDialog(this,
+ "Discard unsaved changes?",
+ "Confirm",
+ JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION;
+}
+----
+
+The generated bridge method has signature `CompletableFuture confirmLeaveCurrentEditor()`. The Swing source itself doesn't import `CompletableFuture` — the codegen does the wrapping.
+
+*Explicit `CompletableFuture` return* — when the Swing dev wants to compose async stages:
+
+[source,java]
+----
+@ExposedMethod
+public CompletableFuture fetchUserGreeting(String userId) {
+ return CompletableFuture
+ .supplyAsync(() -> userService.findGreeting(userId))
+ .thenApplyAsync(g -> g + ", " + LocalDate.now());
+}
+----
+
+
+[[swing-bridge.interop.calling-swing-methods.discovery]]
+== How the Bridge Finds the Target Instance
+
+Before a bridge method can run, the framework needs an _instance_ to invoke it on. That instance is resolved per call using one of three discovery strategies, _inferred_ from the annotated class — there is no `@Discovery` annotation to declare on user code.
+
+[cols="2,3,4", options="header"]
+|===
+| Strategy | Inferred when | Resolution
+
+| `WINDOW`
+| The annotated class extends `java.awt.Window` (so any `JFrame`, `JDialog`, or `JWindow`).
+| The framework uses the most recently visible window of that exact class on the Swing AppContext.
+
+| `SINGLETON`
+| The annotated class has a `@InstanceProvider` static factory, or (legacy) a `public static T getInstance()` method.
+| The factory is invoked on every call to obtain a fresh-or-cached instance.
+
+| `STATIC_ONLY`
+| All `@ExposedMethod` methods on the class are `static` (and the class isn't a `Window`).
+| No instance needed; static dispatch only.
+|===
+
+Mixing `static` and instance `@ExposedMethod` methods on the same class is a build error.
+
+
+[[swing-bridge.interop.calling-swing-methods.discovery.window]]
+=== `WINDOW` — Main Frame and Dialogs
+
+For methods on the application's main `JFrame` or any other `Window` subclass, nothing extra is required:
+
+[source,java]
+----
+public class MainWindow extends JFrame {
+
+ @ExposedMethod
+ public void hideNavigation() { ... }
+}
+----
+
+The bridge resolves to whichever instance of `MainWindow` is currently visible in the Swing AppContext.
+
+
+[[swing-bridge.interop.calling-swing-methods.discovery.instance-provider]]
+=== `SINGLETON` — `@InstanceProvider` for Long-Lived Panels
+
+For a panel held in a customer-side registry — a common pattern in Swing applications that route between "editors" — declare a static factory and annotate it with `@InstanceProvider`:
+
+[source,java]
+----
+package com.example.swingapp.editors;
+
+import com.vaadin.swingbridge.interop.ExposedMethod;
+import com.vaadin.swingbridge.interop.InstanceProvider;
+import javax.swing.JPanel;
+
+public class CustomerPanel extends JPanel {
+
+ @InstanceProvider
+ public static CustomerPanel currentInstance() {
+ return EditorsRegistry.getInstance().get(CustomerPanel.class);
+ }
+
+ @ExposedMethod
+ public CustomerRecord getSelectedCustomer() {
+ return (CustomerRecord) customerList.getSelectedValue();
+ }
+}
+----
+
+Rules for the factory method:
+
+- Must be `public static`.
+- Must take no arguments.
+- Must return the annotated type (or a subtype).
+- Is invoked on _every_ bridge call (not cached), so the registry can return different instances over time.
+- Runs on the EDT — it's safe to touch Swing state inside it.
+
+
+[[swing-bridge.interop.calling-swing-methods.discovery.static-only]]
+=== `STATIC_ONLY` — Utility Classes
+
+For stateless helpers (settings, logging, lookups), make every annotated method `static`:
+
+[source,java]
+----
+public final class Settings {
+ private Settings() {}
+
+ @ExposedMethod
+ public static String getDefaultLocale() {
+ return Locale.getDefault().toLanguageTag();
+ }
+
+ @ExposedMethod
+ public static void setDefaultLocale(String tag) {
+ Locale.setDefault(Locale.forLanguageTag(tag));
+ }
+}
+----
+
+No instance is resolved; the bridge proxy calls the static methods directly.
+
+
+[[swing-bridge.interop.calling-swing-methods.vaadin-side]]
+== Calling the Bridge from Vaadin
+
+Inside a Vaadin view, get a `BridgeHandle` and use it. There are two entry points on the handle, matched to the two shapes of work.
+
+For `void` or `SYNC`-with-result methods, register a one-shot callback with `onReady`:
+
+[source,java]
+----
+import com.example.swingapp.MainWindowBridge;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.Route;
+import com.vaadin.swingbridge.SwingBridge;
+
+@Route("main")
+public class MainView extends VerticalLayout {
+
+ public MainView() {
+ add(new SwingBridge("com.example.swingapp.SwingAppMain"));
+
+ SwingBridge.interop()
+ .of(MainWindowBridge.class)
+ .onReady(main -> main.hideNavigation());
+ }
+}
+----
+
+`onReady` fires once, on the Vaadin UI thread, as soon as the bridge is resolvable. If the bridge is _already_ ready when `onReady` is called (a re-attach, or a different view registering after the Swing app has booted), the callback fires immediately, synchronously, on the calling thread.
+
+For `ASYNC`-shaped methods, use `requestAsync`. It awaits readiness, invokes the method, and relays the result off the Swing EDT so a `whenComplete` continuation can safely call `ui.access(...)`:
+
+[source,java]
+----
+SwingBridge.interop().of(MainWindowBridge.class)
+ .requestAsync(MainWindowBridge::confirmLeaveCurrentEditor)
+ .whenComplete((approved, err) -> getUI().ifPresent(ui -> ui.access(() -> {
+ if (err != null) {
+ Notification.show("Could not confirm: " + err.getMessage());
+ } else if (Boolean.TRUE.equals(approved)) {
+ ui.navigate("home");
+ }
+ })));
+----
+
+[NOTE]
+====
+The off-EDT relay is load-bearing. The Swing-side future is typically completed on the EDT (for example when a `SwingBridgeDialog` closes). Attaching `whenComplete` _directly_ to that future would run the continuation on the EDT, where any `ui.access(...)` resolves inline, re-enters the bridge with `invokeOnEdtAndWait`, and deadlocks the EDT on itself. `requestAsync` relays completion onto `ForkJoinPool.commonPool()` so the standard `ui.access(...)` pattern is safe.
+====
+
+To check readiness without registering a callback — useful in a `BeforeLeaveEvent` handler, for example, where you may want to skip postponing navigation altogether if the Swing app hasn't booted yet:
+
+[source,java]
+----
+if (SwingBridge.interop().of(MainWindowBridge.class).isReady()) {
+ // ...
+}
+----
+
+When more than one Swing application is hosted in the same Vaadin session (rare), disambiguate by passing the main class FQN:
+
+[source,java]
+----
+SwingBridge.interop("com.example.swingapp.SwingAppMain")
+ .of(MainWindowBridge.class)
+ .onReady(...);
+----
+
+
+[[swing-bridge.interop.calling-swing-methods.build]]
+== Build Setup
+
+[[swing-bridge.interop.calling-swing-methods.build.swing-side]]
+=== Swing-Side `pom.xml`
+
+The Swing application's build depends on `swing-bridge-annotations` so that source files can reference `@ExposedMethod`, `@VaadinCallback`, and `@InstanceProvider`:
+
+[source,xml]
+----
+
+
+ com.vaadin
+ swing-bridge-annotations
+ ${swing-bridge.version}
+
+
+----
+
+The annotations are `RUNTIME`-retention; no further changes are needed on the Swing side. The compiled JAR carries the annotation descriptors, which the codegen plugin reads at build time on the Vaadin side.
+
+
+[[swing-bridge.interop.calling-swing-methods.build.vaadin-side]]
+=== Vaadin-Side `pom.xml`
+
+The Vaadin application's build configures the codegen plugin so the generated `*Bridge`, `*BridgeProxy`, and `*BridgeMetadata` classes land on the compile classpath:
+
+[source,xml]
+----
+
+
+
+ com.vaadin
+ swing-bridge-codegen-maven-plugin
+ ${swing-bridge.version}
+
+
+
+ generate-bridge
+
+
+
+
+
+
+----
+
+The goal binds to `generate-sources` by default and:
+
+- Scans every `.jar` in the `applibs` directory (or wherever `` points).
+- Walks the bytecode with ASM, finding classes with `@ExposedMethod`, `@VaadinCallback`, or `@InstanceProvider`.
+- Emits the bridge interface, proxy class, and metadata class per annotated class into `target/generated-sources/swing-bridge/`.
+- Registers the output directory as an additional Maven compile source root so the generated classes are visible to Vaadin code with no extra wiring.
+
+
+[[swing-bridge.interop.calling-swing-methods.errors]]
+== Errors You May See
+
+[cols="3,4", options="header"]
+|===
+| Symptom | What it means
+
+| `SwingBridgeInvocationException`
+| The bridge call could not be dispatched: the target instance couldn't be resolved, the Swing method threw, or the EDT exceeded the timeout. The original throwable is preserved as `cause`.
+
+| `SwingBridgeTimeoutException`
+| Subtype of `SwingBridgeInvocationException`. A `SYNC` non-`void` call exceeded the EDT timeout. Switch the Swing method to `Invocation.ASYNC` so the Vaadin caller doesn't block.
+
+| `IllegalStateException` from `onReady`
+| Called outside a Vaadin UI thread, or called when no Swing AppContext is associated with the bridge yet. Make sure the call happens inside a Vaadin route or `UI.access` block.
+
+| Build error referencing `@InstanceProvider`
+| Codegen could not infer a discovery strategy for the annotated class. Either make all `@ExposedMethod` methods `static`, or add a `@InstanceProvider` factory.
+|===
+
+Continue with <> to wire the other direction — events emitted by Swing into Vaadin handlers.
diff --git a/articles/tools/modernization-toolkit/swing-bridge/interop/index.adoc b/articles/tools/modernization-toolkit/swing-bridge/interop/index.adoc
new file mode 100644
index 0000000000..353a527ecf
--- /dev/null
+++ b/articles/tools/modernization-toolkit/swing-bridge/interop/index.adoc
@@ -0,0 +1,237 @@
+---
+title: Interoperability
+page-title: SwingBridge Interoperability | Vaadin Tools
+description: Type-safe API and tooling to call between Vaadin and an embedded Swing application.
+meta-description: Type-safe interoperability between Vaadin and an embedded Swing application — call Swing methods, react to Swing events, and share domain types.
+order: 4
+---
+
+include::{articles}/_vaadin-version.adoc[]
+
+[[swing-bridge.interop]]
+= Interoperability
+
+Once a Swing application is embedded with SwingBridge (see <<../adding-your-app#, Adding Your Swing Application>>), it runs as a live program inside the Vaadin session. The interoperability layer is what lets a Vaadin view _talk_ to that program: invoke methods on it, listen to events from it, and exchange domain objects with it — type-safely, on the right thread, and without leaking listeners across navigation.
+
+This is the API used during the second phase of an incremental migration, where individual Swing screens are replaced one at a time with native Vaadin views while the rest of the Swing application continues to run unchanged.
+
+
+[[swing-bridge.interop.mental-model]]
+== Mental Model
+
+Interop is bi-directional, and each direction has its own annotation:
+
+[cols="1,3,3", options="header"]
+|===
+| Direction | What it does | Annotation
+
+| Vaadin → Swing
+| A Vaadin view calls a method on a Swing class. The call is dispatched onto the AWT Event Dispatch Thread (EDT). The return value (or `CompletableFuture`) flows back to Vaadin.
+| `@ExposedMethod` on the Swing-side method.
+
+| Swing → Vaadin
+| Swing code invokes a listener interface. The framework proxies the listener onto the Vaadin UI thread, where a handler method on the Vaadin view receives the event.
+| `@VaadinCallback` on the Vaadin-side handler.
+|===
+
+A Maven plugin scans the Swing application's JARs at build time and generates a typed bridge interface, proxy, and metadata class for each annotated Swing class. Vaadin code obtains a handle to that bridge through `SwingBridge.interop().of(MyBridge.class)` and works with the generated interface as if it were a normal Java API.
+
+
+[[swing-bridge.interop.ingredients]]
+== Ingredients
+
+[cols="2,5", options="header"]
+|===
+| Element | Role
+
+| `@ExposedMethod` (Swing side)
+| Marks a public Swing method as callable from Vaadin. `invocation()` selects between blocking (`SYNC`) and `CompletableFuture`-wrapped (`ASYNC`) call shapes.
+
+| `@VaadinCallback` (Vaadin side)
+| Marks a handler method on a Vaadin view. `observerFor()` names the Swing listener interface it implements; `dispatch()` selects how the event hops onto the Vaadin UI thread.
+
+| `@InstanceProvider` (Swing side)
+| Marks a `public static` no-arg factory method that returns the live target instance. Required when the annotated Swing class isn't a `Window` and isn't fully static.
+
+| `SwingBridge.interop()`
+| Static accessor that returns the session-scoped `SwingBridgeInterop` for the (only) Swing app running in the current Vaadin session. Use the `interop(mainClassFQN)` overload when more than one Swing app is hosted in the same session.
+
+| `BridgeHandle`
+| Returned by `SwingBridge.interop().of(MyBridge.class)`. Bundles `onReady`, `requestAsync`, `registerCallback`, and `isReady` so the bridge type is named once at the call site.
+
+| `swing-bridge-codegen-maven-plugin`
+| Maven plugin that scans annotated Swing JARs and emits `*Bridge`, `*BridgeProxy`, and `*BridgeMetadata` classes into `target/generated-sources/swing-bridge/`.
+|===
+
+
+[[swing-bridge.interop.decision-tree]]
+== Which Page Do I Need?
+
+Use this decision tree to find the right starting point:
+
+. *A Vaadin view needs to call a Swing method.* +
+ → <>. Covers `@ExposedMethod`, the four invocation shapes, instance discovery, and the `BridgeHandle.onReady` / `requestAsync` call sites.
+
+. *A Vaadin view needs to react to a Swing event.* +
+ → <>. Covers `@VaadinCallback`, the three `Dispatch` modes, setter-vs-adder/remover wiring shapes, and the `Registration` cleanup pattern.
+
+. *A method on either side accepts or returns a Swing domain object (entity, DTO, value type).* +
+ → <>. This is the single most important section. Choosing the wrong strategy here causes silent `ClassCastException` or runtime classloader failures.
+
+. *Replacing a deeply-nested Swing dialog with a Vaadin one, or wiring up a Vaadin-driven cross-cutting concern.* +
+ → <>. Vaadin-rendered LOV dialog, navigation guard with `BeforeLeaveObserver`, calling a bridge from a view that doesn't host the Swing component, and the `SwingBridge.runInAppContext(...)` escape hatch.
+
+
+[[swing-bridge.interop.prerequisites]]
+== Prerequisites
+
+- A working SwingBridge setup. If you don't have one yet, follow <<../quick-start-guide#, Quick Start>> or <<../installation-from-scratch#, Installation from Scratch>>.
+- The Swing application's compile classpath has the `swing-bridge-annotations` JAR on it — see <<#swing-bridge.interop.prerequisites.annotations, Adding the Annotations JAR>> below for how to do this in Maven, Ant, Gradle, or directly from your IDE.
+- The Vaadin application's build configures the `swing-bridge-codegen-maven-plugin` so generated bridge interfaces are on the Vaadin compile classpath. See <>.
+
+
+[[swing-bridge.interop.prerequisites.annotations]]
+=== Adding the Annotations JAR to the Swing Application
+
+The annotations live in a single JAR with these coordinates:
+
+[cols="1,3", options="header"]
+|===
+| Coordinate | Value
+
+| Group ID
+| `com.vaadin`
+
+| Artifact ID
+| `swing-bridge-annotations`
+
+| Version
+| `{swing-bridge-version}` — use the version that matches the SwingBridge release on the Vaadin side of the project. The two sides must stay in lockstep.
+|===
+
+The JAR carries only the `@ExposedMethod`, `@VaadinCallback`, and `@InstanceProvider` annotations, plus the `Invocation`, `Dispatch`, and `Discovery` enums. It has no transitive dependencies and no runtime cost — it is *compile-only* on the Swing side. The Swing application does not need it on its runtime classpath; the embedded launch on the Vaadin side supplies the runtime classes.
+
+The JAR is published in Vaadin's public Maven repositories — `https://maven.vaadin.com/vaadin-addons` for stable releases and `https://maven.vaadin.com/vaadin-prereleases` for pre-releases. Downloads are anonymous; the SwingBridge license check happens later, at runtime on the Vaadin side, when the embedded Swing app starts. See <<../installation-from-scratch#swing-bridge.installation-from-scratch.license, License Installation>> for the license setup itself.
+
+Pick the approach that matches your Swing project's build:
+
+[[swing-bridge.interop.prerequisites.annotations.maven]]
+==== Maven
+
+[source,xml,subs="+attributes"]
+----
+
+
+ vaadin-addons
+ https://maven.vaadin.com/vaadin-addons
+
+
+
+
+
+
+ com.vaadin
+ swing-bridge-annotations
+ {swing-bridge-version}
+ provided
+
+
+----
+
+`scope=provided` reflects the compile-only nature of the JAR. The `` block is only needed if your Swing project's `pom.xml` doesn't already inherit one from a parent.
+
+[[swing-bridge.interop.prerequisites.annotations.gradle]]
+==== Gradle
+
+[source,groovy,subs="+attributes"]
+----
+repositories {
+ maven { url = uri("https://maven.vaadin.com/vaadin-addons") }
+ // ... existing repositories ...
+}
+
+dependencies {
+ compileOnly 'com.vaadin:swing-bridge-annotations:{swing-bridge-version}'
+}
+----
+
+`compileOnly` is the Gradle equivalent of `provided`.
+
+[[swing-bridge.interop.prerequisites.annotations.manual]]
+==== Ant, IDE-Managed `lib/`, or No Formal Build Tool
+
+Most long-running Swing codebases use Ant, IDE-managed libraries, or no formal build tool. Download the JAR directly with a browser or `curl`:
+
+[source,terminal,subs="+attributes"]
+----
+curl -O https://maven.vaadin.com/vaadin-addons/com/vaadin/swing-bridge-annotations/{swing-bridge-version}/swing-bridge-annotations-{swing-bridge-version}.jar
+----
+
+For a SwingBridge pre-release, swap `vaadin-addons` for `vaadin-prereleases` in the URL. The download is anonymous — no Vaadin license check at this step.
+
+Then add the JAR to the Swing project:
+
+. *Drop it into the project's compile-time library folder.* Wherever you keep compile-time JARs today — typically `lib/`, `libs/`, or `dependencies/` next to the project.
+
+. *Reference it from the build:*
++
+- *Ant*: add the JAR to the path used by the `` task. Most projects expose a `` block; add a `` entry inside it.
+- *Eclipse* (with or without Ant): right-click the project → _Build Path_ → _Configure Build Path_ → _Libraries_ → _Add External JARs_ → select the JAR. If the project is shared in version control, commit the resulting `.classpath` change.
+- *IntelliJ IDEA*: _File_ → _Project Structure_ → _Libraries_ → _+ Java_ → select the JAR; assign it to the relevant module(s).
+- *NetBeans*: right-click the project → _Properties_ → _Libraries_ → _Compile_ tab → _Add JAR/Folder_ → select the JAR.
+
+. *Verify.* Open any Swing source file and confirm that this import resolves with no error:
++
+[source,java]
+----
+import com.vaadin.swingbridge.interop.ExposedMethod;
+----
+
+
+[[swing-bridge.interop.status]]
+== What's Shipped Today
+
+The interop runtime, the `generate-bridge` Maven goal, and the by-reference domain-types strategy are production-ready. Two further codegen goals exist as in-progress skeletons and are not recommended for production use yet:
+
+[cols="3,1,4", options="header"]
+|===
+| Capability | Status | Notes
+
+| `@ExposedMethod`, `@VaadinCallback`, `@InstanceProvider`
+| Shipped
+| Stable annotation surface in `com.vaadin.swingbridge.interop`.
+
+| `SwingBridge.interop()` / `BridgeHandle`
+| Shipped
+| Including `onReady`, `requestAsync`, `registerCallback`, and `isReady`.
+
+| `SwingBridge.runInAppContext(Component, Runnable \| Callable)`
+| Shipped
+| Helper for running arbitrary code on a thread in the Swing app's `AppContext`.
+
+| `generate-bridge` Maven goal
+| Shipped
+| Scans JARs, infers discovery, emits bridge interface + proxy + metadata + listener-interface stubs.
+
+| Shared domain types by reference (Type 3+)
+| Shipped
+| Add the domain JAR as a Maven `` of the Vaadin app. Same `Class>` on both sides; no conversion.
+
+| `strip-annotations` Maven goal (Type 1)
+| Roadmap
+| Mojo skeleton in the codebase but untested and unwired. Don't rely on it yet.
+
+| `generate-shared-types` Maven goal (Type 2)
+| Roadmap
+| Mojo skeleton for DTO generation + JSON marshalling. Currently requires hand-rolled DTOs as an interim.
+
+| Gradle plugin wrapper
+| Not started
+| Maven only for now.
+|===
+
+
+[[swing-bridge.interop.topics]]
+== Topics
+
+section_outline::[]
diff --git a/articles/tools/modernization-toolkit/swing-bridge/interop/listening-to-swing-events.adoc b/articles/tools/modernization-toolkit/swing-bridge/interop/listening-to-swing-events.adoc
new file mode 100644
index 0000000000..12d428e0b2
--- /dev/null
+++ b/articles/tools/modernization-toolkit/swing-bridge/interop/listening-to-swing-events.adoc
@@ -0,0 +1,294 @@
+---
+title: Listening to Swing Events
+page-title: Handle Swing Events in Vaadin | SwingBridge
+description: Implement Swing listener interfaces with @VaadinCallback handlers on a Vaadin view.
+meta-description: Handle Swing events on a Vaadin view with @VaadinCallback, choose a dispatch mode, and clean up listener registrations on detach.
+order: 2
+---
+
+include::{articles}/_vaadin-version.adoc[]
+
+[[swing-bridge.interop.listening-to-swing-events]]
+= Listening to Swing Events from Vaadin
+
+When a Swing component fires an event — a table selection, a value change, an audit notification — a Vaadin view can subscribe to it by implementing the Swing-side listener interface as a `@VaadinCallback`-annotated method. The framework builds a proxy that hops onto the Vaadin UI thread and invokes the handler.
+
+
+[[swing-bridge.interop.listening-to-swing-events.swing-side]]
+== Two Wiring Shapes on the Swing Side
+
+The Swing class needs to accept a listener instance. Two patterns are supported, and the choice affects whether the Vaadin side gets clean detach behaviour.
+
+[[swing-bridge.interop.listening-to-swing-events.swing-side.setter]]
+=== Setter Style — Single Observer, No Cleanup
+
+A method named `setXxxListener(L l)`-shaped (one parameter of a listener-interface type):
+
+[source,java]
+----
+package com.example.swingapp;
+
+import com.vaadin.swingbridge.interop.ExposedMethod;
+import com.vaadin.swingbridge.interop.InstanceProvider;
+
+public class CustomerPanel extends javax.swing.JPanel {
+
+ private CustomerSelectionListener listener;
+
+ @InstanceProvider
+ public static CustomerPanel currentInstance() {
+ return EditorsRegistry.getInstance().get(CustomerPanel.class);
+ }
+
+ @ExposedMethod
+ public void setCustomerSelectionListener(CustomerSelectionListener l) {
+ this.listener = l;
+ }
+
+ private void onTableSelectionChanged() {
+ Customer c = (Customer) table.getSelectedValue();
+ if (listener != null) {
+ listener.onCustomerSelected(c);
+ }
+ }
+}
+----
+
+[[swing-bridge.interop.listening-to-swing-events.swing-side.adder]]
+=== Adder/Remover Style — Multi-Observer with Cleanup
+
+The annotated method takes the listener and _returns a `Runnable`_ whose `run()` removes the listener. This is the preferred shape:
+
+[source,java]
+----
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class CustomerPanel extends javax.swing.JPanel {
+
+ private final List listeners =
+ new CopyOnWriteArrayList<>();
+
+ @InstanceProvider
+ public static CustomerPanel currentInstance() { ... }
+
+ @ExposedMethod
+ public Runnable addCustomerSelectionListener(CustomerSelectionListener l) {
+ listeners.add(l);
+ return () -> listeners.remove(l);
+ }
+
+ private void onTableSelectionChanged() {
+ Customer c = (Customer) table.getSelectedValue();
+ for (CustomerSelectionListener l : listeners) {
+ l.onCustomerSelected(c);
+ }
+ }
+}
+----
+
+The codegen recognises a `Runnable` return type as a remover and captures it so the Vaadin side can run the cleanup later. Multiple views can subscribe to the same panel independently.
+
+[NOTE]
+====
+Codegen rejects `@ExposedMethod(invocation = Invocation.ASYNC)` on adder/remover-style methods. The `Runnable` is captured eagerly during registration, so the call shape has to be synchronous.
+====
+
+
+[[swing-bridge.interop.listening-to-swing-events.vaadin-side]]
+== Handling the Event on the Vaadin Side
+
+On the Vaadin view, declare the listener interface's method body and annotate it with `@VaadinCallback`. `observerFor()` names the listener interface:
+
+[source,java]
+----
+import com.example.swingapp.CustomerPanelBridge;
+import com.example.swingapp.CustomerSelectionListener;
+import com.example.swingapp.Customer;
+import com.vaadin.flow.component.AttachEvent;
+import com.vaadin.flow.component.DetachEvent;
+import com.vaadin.flow.component.notification.Notification;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.shared.Registration;
+import com.vaadin.swingbridge.SwingBridge;
+import com.vaadin.swingbridge.interop.VaadinCallback;
+
+@Route("customers")
+public class CustomersView extends VerticalLayout {
+
+ private Registration callbackRegistration;
+
+ @Override
+ protected void onAttach(AttachEvent attachEvent) {
+ super.onAttach(attachEvent);
+ callbackRegistration = SwingBridge.interop()
+ .of(CustomerPanelBridge.class)
+ .registerCallback(this);
+ }
+
+ @Override
+ protected void onDetach(DetachEvent detachEvent) {
+ if (callbackRegistration != null) {
+ callbackRegistration.remove();
+ callbackRegistration = null;
+ }
+ super.onDetach(detachEvent);
+ }
+
+ @VaadinCallback(observerFor = CustomerSelectionListener.class)
+ public void onCustomerSelected(Customer customer) {
+ Notification.show("Selected: " + customer.getName());
+ }
+}
+----
+
+`registerCallback(this)`:
+
+- Walks the bridge interface for every one-argument method whose parameter is a listener interface.
+- For each match, finds a `@VaadinCallback` method on `this` whose `observerFor` equals that listener interface.
+- Builds a proxy that forwards every method invocation onto the Vaadin UI thread according to the handler's `dispatch()` setting.
+- Calls the Swing-side setter (or adder) with the proxy.
+- Returns a single `Registration` whose `remove()` runs every captured cleanup `Runnable`.
+
+
+[[swing-bridge.interop.listening-to-swing-events.cleanup]]
+== The Cleanup Pattern
+
+Store the returned `Registration` and call `.remove()` from `onDetach`. With adder/remover-style Swing wiring, every navigation cycle would otherwise add another listener to the Swing panel, and the panel would keep firing events into detached UIs until the session is destroyed.
+
+[source,java]
+----
+private Registration callbackRegistration;
+
+@Override
+protected void onAttach(AttachEvent e) {
+ super.onAttach(e);
+ callbackRegistration = SwingBridge.interop()
+ .of(CustomerPanelBridge.class)
+ .registerCallback(this);
+}
+
+@Override
+protected void onDetach(DetachEvent e) {
+ if (callbackRegistration != null) {
+ callbackRegistration.remove();
+ callbackRegistration = null;
+ }
+ super.onDetach(e);
+}
+----
+
+For setter-style wiring there's nothing to remove (the next `registerCallback` overwrites the previous proxy), but calling `.remove()` is still safe and harmless — write the same code regardless so a future switch from setter to adder/remover doesn't introduce a leak silently.
+
+
+[[swing-bridge.interop.listening-to-swing-events.dispatch-modes]]
+== Choosing a Dispatch Mode
+
+`@VaadinCallback(dispatch = ...)` chooses how the proxy hands the call off from the Swing thread to the Vaadin UI thread. The three modes map verbatim to `UI` methods:
+
+[cols="2,2,4", options="header"]
+|===
+| Mode | Underlying call | When to use
+
+| `Dispatch.ACCESS` (default)
+| `ui.access(task)`
+| Default for `void` listener methods. Fire-and-forget. Throws on the Swing side if the UI is detached when the proxy is invoked.
+
+| `Dispatch.ACCESS_SYNCHRONOUSLY`
+| `ui.accessSynchronously(task)`
+| _Required_ for listener methods with a non-`void` return type. The Swing caller blocks until the handler completes, and the return value (or thrown exception) flows back to Swing.
+
+| `Dispatch.ACCESS_LATER`
+| `ui.accessLater(task, detachHandler)`
+| Detach-safe fire-and-forget. If the UI is detached when the proxy is invoked, the event is silently dropped. Right for audit logs and best-effort telemetry where lost events on navigation are acceptable.
+|===
+
+The framework checks the listener method's return type at registration and throws if a non-`void` method is bound with anything other than `ACCESS_SYNCHRONOUSLY`.
+
+[[swing-bridge.interop.listening-to-swing-events.dispatch-modes.access-synchronously]]
+=== Returning a Value with `ACCESS_SYNCHRONOUSLY`
+
+This pattern enables Vaadin to answer questions Swing asks. The listener interface declares a non-`void` return:
+
+[source,java]
+----
+// On the Swing side
+public interface CityChooser {
+ CompletableFuture choose(String currentZip);
+}
+----
+
+The Vaadin handler implements it with `ACCESS_SYNCHRONOUSLY` so the return value can propagate back:
+
+[source,java]
+----
+@VaadinCallback(observerFor = CityChooser.class,
+ dispatch = Dispatch.ACCESS_SYNCHRONOUSLY)
+public CompletableFuture choose(String currentZip) {
+ CompletableFuture result = new CompletableFuture<>();
+ new CityLookupDialog(currentZip, result::complete).open();
+ return result; // unfinished; completed when the user picks a row
+}
+----
+
+The Swing caller's thread is parked _just long enough_ for the handler to open the dialog and return the unfinished future. The dialog completes the future later from the Vaadin UI thread.
+
+This is the foundation of the Vaadin-rendered list-of-values pattern. The full end-to-end example is in <>.
+
+
+[[swing-bridge.interop.listening-to-swing-events.dispatch-modes.access-later]]
+=== Surviving Detach with `ACCESS_LATER`
+
+For events that may fire while the user is navigating away, choose `ACCESS_LATER`:
+
+[source,java]
+----
+@VaadinCallback(observerFor = CaseAuditListener.class,
+ dispatch = Dispatch.ACCESS_LATER)
+public void onCaseAction(String actionName) {
+ auditLog.record(actionName, getCurrentUser());
+}
+----
+
+If the UI is still attached, the handler runs on the UI thread. If the UI has detached by the time Swing fires the event, the framework drops it silently — no exception, no log noise.
+
+
+[[swing-bridge.interop.listening-to-swing-events.multi-listener]]
+== One View, Multiple Listener Slots
+
+A single `registerCallback` call wires every matching slot on the bridge. If a Swing panel exposes three adders for three different listener interfaces, declare one `@VaadinCallback` per interface on the view and call `registerCallback(this)` once:
+
+[source,java]
+----
+public class CustomersView extends VerticalLayout {
+
+ private Registration callbackRegistration;
+
+ @Override
+ protected void onAttach(AttachEvent e) {
+ super.onAttach(e);
+ callbackRegistration = SwingBridge.interop()
+ .of(CustomerEditorBridge.class)
+ .registerCallback(this);
+ }
+
+ @VaadinCallback(observerFor = CustomerSelectionListener.class)
+ public void onCustomerSelected(Customer c) { ... }
+
+ @VaadinCallback(observerFor = CustomerFilterListener.class)
+ public void onCustomerFilterChanged(String filterText) { ... }
+
+ @VaadinCallback(observerFor = CustomerModeChangeListener.class)
+ public void onCustomerModeChanged(EditorMode mode) { ... }
+}
+----
+
+The returned `Registration` cleans up all three slots when `.remove()` is called.
+
+
+[[swing-bridge.interop.listening-to-swing-events.next]]
+== Next
+
+- If listener interfaces or their methods carry your own domain types, read <> next.
+- For a complete end-to-end pattern that combines exposed methods _and_ callbacks, see <>.
diff --git a/articles/tools/modernization-toolkit/swing-bridge/interop/patterns.adoc b/articles/tools/modernization-toolkit/swing-bridge/interop/patterns.adoc
new file mode 100644
index 0000000000..f50f773303
--- /dev/null
+++ b/articles/tools/modernization-toolkit/swing-bridge/interop/patterns.adoc
@@ -0,0 +1,315 @@
+---
+title: Patterns and Cookbook
+page-title: SwingBridge Interop Patterns | Vaadin Tools
+description: Common high-level patterns for combining @ExposedMethod, @VaadinCallback, and the SwingBridge helpers.
+meta-description: Recipes for the Vaadin-rendered LOV dialog, navigation guards, bridges from non-host views, and the runInAppContext escape hatch.
+order: 4
+---
+
+include::{articles}/_vaadin-version.adoc[]
+
+[[swing-bridge.interop.patterns]]
+= Patterns and Cookbook
+
+The previous pages describe individual interop _ingredients_. This page combines them into common end-to-end patterns.
+
+
+[[swing-bridge.interop.patterns.lov]]
+== Vaadin-Rendered List-of-Values Dialog
+
+A frequent goal during incremental migration is replacing a deeply-nested Swing lookup dialog with a modern Vaadin dialog, without modifying the Swing form that opens it. The flagship pattern combines `@VaadinCallback(dispatch = ACCESS_SYNCHRONOUSLY)` (so Vaadin can return a value to Swing) with `@ExposedMethod(invocation = ASYNC)` (so the dialog can call back into Swing for data).
+
+*Swing side — the listener interface and the form that uses it:*
+
+[source,java]
+----
+package com.example.swingapp;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface CityChooser {
+ CompletableFuture choose(String currentZip);
+}
+----
+
+[source,java]
+----
+package com.example.swingapp;
+
+import com.vaadin.swingbridge.interop.ExposedMethod;
+import com.vaadin.swingbridge.interop.Invocation;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+public class NewCasePanel extends JPanel {
+
+ private CityChooser cityChooser;
+
+ @ExposedMethod
+ public void setCityChooser(CityChooser chooser) {
+ this.cityChooser = chooser;
+ }
+
+ @ExposedMethod(invocation = Invocation.ASYNC)
+ public CityDataBean[] searchCityData(String query) {
+ return cityService.search(query).toArray(new CityDataBean[0]);
+ }
+
+ private void onMagnifierClicked() {
+ if (cityChooser == null) return;
+ cityChooser.choose(zipField.getText()).whenComplete((bean, err) -> {
+ if (err != null || bean == null) return;
+ SwingUtilities.invokeLater(() -> {
+ zipField.setText(bean.getZipCode());
+ cityField.setText(bean.getCity());
+ });
+ });
+ }
+}
+----
+
+*Vaadin side — the handler and the dialog:*
+
+[source,java]
+----
+import com.example.swingapp.CityChooser;
+import com.example.swingapp.NewCasePanelBridge;
+import com.example.domain.CityDataBean;
+import com.vaadin.flow.component.AttachEvent;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.shared.Registration;
+import com.vaadin.swingbridge.SwingBridge;
+import com.vaadin.swingbridge.interop.Dispatch;
+import com.vaadin.swingbridge.interop.VaadinCallback;
+import java.util.concurrent.CompletableFuture;
+
+public class NewCaseView extends VerticalLayout {
+
+ private Registration callbackRegistration;
+
+ @Override
+ protected void onAttach(AttachEvent attachEvent) {
+ super.onAttach(attachEvent);
+ callbackRegistration = SwingBridge.interop()
+ .of(NewCasePanelBridge.class)
+ .registerCallback(this);
+ }
+
+ @VaadinCallback(observerFor = CityChooser.class,
+ dispatch = Dispatch.ACCESS_SYNCHRONOUSLY)
+ public CompletableFuture choose(String currentZip) {
+ CompletableFuture result = new CompletableFuture<>();
+ new CityLookupDialog(currentZip, result::complete).open();
+ return result;
+ }
+}
+----
+
+[source,java]
+----
+import com.example.domain.CityDataBean;
+import com.example.swingapp.NewCasePanelBridge;
+import com.vaadin.flow.component.Key;
+import com.vaadin.flow.component.dialog.Dialog;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.swingbridge.SwingBridge;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class CityLookupDialog extends Dialog {
+
+ private final TextField queryField = new TextField("Search");
+ private final Grid grid = new Grid<>();
+ private final Consumer onPick;
+ private boolean completed;
+
+ public CityLookupDialog(String initialQuery, Consumer onPick) {
+ this.onPick = onPick;
+ queryField.setValue(initialQuery);
+
+ grid.addColumn(CityDataBean::getZipCode).setHeader("Zip");
+ grid.addColumn(CityDataBean::getCity).setHeader("City");
+ grid.addItemDoubleClickListener(e -> closeWith(e.getItem()));
+
+ queryField.addKeyPressListener(Key.ENTER, e -> search(queryField.getValue()));
+ addOpenedChangeListener(e -> { if (!e.isOpened()) closeWith(null); });
+
+ add(queryField, grid);
+ search(initialQuery);
+ }
+
+ private void search(String q) {
+ SwingBridge.interop().of(NewCasePanelBridge.class)
+ .requestAsync(b -> b.searchCityData(q))
+ .whenComplete((rows, err) -> getUI().ifPresent(ui -> ui.access(() -> {
+ grid.setItems(rows == null ? List.of() : Arrays.asList(rows));
+ })));
+ }
+
+ private void closeWith(CityDataBean picked) {
+ if (completed) return;
+ completed = true;
+ onPick.accept(picked);
+ close();
+ }
+}
+----
+
+A few details that make this pattern hold together:
+
+- `Dispatch.ACCESS_SYNCHRONOUSLY` is what lets the Vaadin handler return a `CompletableFuture` value to the Swing caller. With any other dispatch mode the framework rejects the registration.
+- The handler returns the _unfinished_ future immediately — that's the whole point. The Swing thread unparks within milliseconds; the future is completed later, from the Vaadin UI thread, when the user picks a row or closes the dialog.
+- `requestAsync` inside the dialog relays the search result off the Swing EDT, so the `whenComplete` continuation can safely call `ui.access(...)` without re-entering the bridge and deadlocking.
+- The Swing-side `whenComplete` hops back via `SwingUtilities.invokeLater` before touching Swing UI state — the completion thread is _not_ the EDT.
+
+
+[[swing-bridge.interop.patterns.nav-guard]]
+== Navigation Guard with `BeforeLeaveObserver`
+
+When the user navigates away from a Vaadin route that hosts a Swing form with unsaved edits, you typically want to ask the Swing side whether it's safe to leave. The pattern: postpone the navigation, call an `ASYNC`-shaped Swing method via `requestAsync`, then proceed or cancel based on the answer.
+
+*Swing side:*
+
+[source,java]
+----
+@ExposedMethod(invocation = Invocation.ASYNC)
+public Boolean confirmLeaveCurrentEditor() {
+ return EditorRegistry.current().canLeave();
+}
+----
+
+*Vaadin side:*
+
+[source,java]
+----
+import com.example.swingapp.MainWindowBridge;
+import com.vaadin.flow.router.BeforeEnterEvent;
+import com.vaadin.flow.router.BeforeEnterObserver;
+import com.vaadin.flow.router.BeforeLeaveEvent;
+import com.vaadin.flow.router.BeforeLeaveEvent.ContinueNavigationAction;
+import com.vaadin.flow.router.BeforeLeaveObserver;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.swingbridge.SwingBridge;
+
+public abstract class SwingEditorView extends VerticalLayout
+ implements BeforeLeaveObserver, BeforeEnterObserver {
+
+ private static volatile Class extends Component> pendingCancelRedirect;
+
+ @Override
+ public void beforeLeave(BeforeLeaveEvent event) {
+ var handle = SwingBridge.interop().of(MainWindowBridge.class);
+ if (!handle.isReady()) return; // Swing not booted yet — just let it through
+
+ ContinueNavigationAction action = event.postpone();
+ handle.requestAsync(MainWindowBridge::confirmLeaveCurrentEditor)
+ .whenComplete((approved, err) -> getUI().ifPresent(ui -> ui.access(() -> {
+ if (err != null) {
+ action.proceed(); // fail open — don't trap the user
+ return;
+ }
+ if (Boolean.TRUE.equals(approved)) {
+ action.proceed();
+ } else {
+ // Stash the source view so beforeEnter can redirect back.
+ pendingCancelRedirect = getClass();
+ action.proceed();
+ }
+ })));
+ }
+
+ @Override
+ public void beforeEnter(BeforeEnterEvent event) {
+ Class extends Component> redirect = pendingCancelRedirect;
+ if (redirect != null) {
+ pendingCancelRedirect = null;
+ event.forwardTo(redirect);
+ }
+ }
+}
+----
+
+The non-obvious detail is the cancellation path: when the Swing side says "no", we _still_ call `action.proceed()` and then forward back to the original route from the target view's `beforeEnter`. Postponing without later calling `proceed()` leaves Vaadin's client-side router in a state where subsequent `RouterLink` clicks are silently dropped.
+
+
+[[swing-bridge.interop.patterns.non-host-view]]
+== Calling a Bridge from a View That Doesn't Host the Swing App
+
+The `SwingBridgeInterop` registry is scoped to the Vaadin session and persists across `SwingBridge` attach/detach. Any view in the session can call any `SINGLETON` or `STATIC_ONLY` bridge — even views that have nothing to do with the embedded Swing component.
+
+[source,java]
+----
+@Route("admin")
+public class AdminView extends VerticalLayout {
+
+ public AdminView() {
+ add(new Button("Show default locale", e -> {
+ SwingBridge.interop()
+ .of(SettingsBridge.class)
+ .requestAsync(SettingsBridge::getDefaultLocale)
+ .whenComplete((locale, err) -> getUI().ifPresent(ui ->
+ ui.access(() -> Notification.show("Locale: " + locale))));
+ }));
+ }
+}
+----
+
+`WINDOW`-typed bridges (those whose target is a `JFrame` or other `Window`) work the same way as long as a matching window has been seen at least once in the session — the framework caches the last-seen frame. If no `SwingBridge` has ever attached in the session, `WINDOW` bridges aren't ready and `onReady` simply queues the callback for later.
+
+
+[[swing-bridge.interop.patterns.runinappcontext]]
+== `SwingBridge.runInAppContext` — The AppContext Escape Hatch
+
+Sometimes there's no `@ExposedMethod` to call but you still need to run something inside the Swing app's `AppContext` — for example, to apply a Look-and-Feel workaround before the first window is shown, or to drive a piece of legacy code that depends on `AppContext`-bound static state.
+
+`SwingBridge` exposes two static helpers for this. Both run the task on a fresh thread in the Swing app's `ThreadGroup`, not on the EDT.
+
+*Fire-and-forget:*
+
+[source,java]
+----
+SwingBridge.runInAppContext(swingComponent, () -> {
+ // Runs in the right AppContext, but not on the EDT.
+ // Hop to the EDT manually if you need to touch Swing UI state:
+ EventQueue.invokeLater(() -> {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ });
+});
+----
+
+*With a result:*
+
+[source,java]
+----
+CompletableFuture future = SwingBridge.runInAppContext(swingComponent, () -> {
+ // Callable — return value or thrown exception flows back through the future.
+ return LegacyConfigService.snapshot();
+});
+
+future.whenComplete((snapshot, err) -> getUI().ifPresent(ui -> ui.access(() -> {
+ if (err != null) {
+ Notification.show("Snapshot failed: " + err.getMessage());
+ } else {
+ snapshotPanel.render(snapshot);
+ }
+})));
+----
+
+If no `AppContext` is associated with the component (the Swing app hasn't started, or the component isn't from a bridged Swing app), the `Runnable` overload is a silent no-op, and the `Callable` overload completes the future exceptionally with `IllegalStateException`.
+
+[NOTE]
+====
+*The task does not run on the EDT.* `runInAppContext` only places the task inside the right `AppContext` `ThreadGroup`. To touch any Swing UI state, the task itself must call `EventQueue.invokeLater(...)` or `EventQueue.invokeAndWait(...)`.
+
+For ordinary cross-bridge calls — invoking a Swing method, reading a value, listening for events — prefer `@ExposedMethod` + `BridgeHandle.requestAsync`. `runInAppContext` is the escape hatch for one-off setup that doesn't fit the annotation model.
+====
+
+
+[[swing-bridge.interop.patterns.next]]
+== Next
+
+- For the underlying mechanics of each ingredient used here, return to <> and <>.
+- To make sure your domain entities cross the bridge correctly, read <>.
diff --git a/articles/tools/modernization-toolkit/swing-bridge/interop/sharing-domain-types.adoc b/articles/tools/modernization-toolkit/swing-bridge/interop/sharing-domain-types.adoc
new file mode 100644
index 0000000000..ed15268543
--- /dev/null
+++ b/articles/tools/modernization-toolkit/swing-bridge/interop/sharing-domain-types.adoc
@@ -0,0 +1,305 @@
+---
+title: Sharing Domain Types
+page-title: Share Swing Domain Types with Vaadin | SwingBridge
+description: Make Swing entity classes resolvable from both sides so they cross the bridge without conversion.
+meta-description: Decide how Swing domain types cross the SwingBridge boundary — same-class by reference, annotation-stripped, or DTO-converted — without ClassCastException.
+order: 3
+---
+
+include::{articles}/_vaadin-version.adoc[]
+
+[[swing-bridge.interop.sharing-domain-types]]
+= Sharing Domain Types
+
+This page covers the single most important decision when adding interop on top of an embedded Swing application: _what happens when a bridge method accepts or returns a domain object_, like `Customer`, `CityDataBean`, or `Order`.
+
+If both sides of the bridge resolve `com.example.Customer` to the _same_ `Class>` object, the entity crosses by reference and Vaadin code can use it directly. If they resolve to _different_ `Class>` objects (same fully-qualified name, different classloaders), the very first cross-boundary call fails with `ClassCastException`. The strategy you pick determines which world you live in.
+
+
+[[swing-bridge.interop.sharing-domain-types.classloader-model]]
+== The Classloader Model
+
+To keep each Vaadin session's Swing app isolated, SwingBridge loads the Swing JARs into a per-session `URLClassLoader`. That classloader's _parent_ is the thread's context classloader at the moment the Swing app starts — typically the Vaadin application's own classloader (in dev mode, Spring Boot's `RestartClassLoader`; in production, the launcher classloader).
+
+This setup gives a single invariant to design around:
+
+[NOTE]
+====
+*Any class on the Vaadin app's parent classpath is visible to the Swing-side classloader through parent-first delegation, and resolves to the same `Class>` object on both sides.*
+
+Conversely, a class that lives only in `applibs/` (the Swing-only JARs) is _not_ visible to the Vaadin classpath and cannot cross the bridge by reference.
+====
+
+Listener interfaces are an automatic exception. Codegen reads the Swing JAR at build time and emits a stub of every non-JDK reference-type interface used in an `@ExposedMethod` or `@VaadinCallback` signature into `target/generated-sources/swing-bridge/`. So a custom listener interface like `CustomerSelectionListener` "just works" with no manual sharing — Vaadin code compiles against the generated stub and resolves it through parent-first delegation at runtime.
+
+What does _not_ "just work" automatically is data classes — entities, value types, DTOs. Those are the focus of this page.
+
+
+[[swing-bridge.interop.sharing-domain-types.application-profiles]]
+== Classify the Swing Application First
+
+The right strategy depends on the shape of the Swing application's domain JAR:
+
+[cols="1,4,3", options="header"]
+|===
+| Profile | What it looks like | Suggested strategy
+
+| Type 3
+| No domain types cross the bridge. All `@ExposedMethod` signatures use JDK types only (`String`, `int`, arrays of primitives, etc.).
+| Nothing extra — covered automatically.
+
+| Type 3+
+| A separate, _thin_ domain JAR (entity beans only, no embedded Hibernate / EJB / JNDI startup). The Vaadin app starts cleanly with that JAR on its classpath.
+| <<#swing-bridge.interop.sharing-domain-types.type-3plus, By reference>>. _Shipped._
+
+| Type 1
+| A thin domain JAR, but it carries `javax.persistence` / `javax.validation` / `javax.ejb` annotations that conflict with the Vaadin app's `jakarta.*` stack.
+| <<#swing-bridge.interop.sharing-domain-types.type-1, Strip annotations>>. _Roadmap._
+
+| Type 2
+| Fat client. Domain types and an embedded ORM (Hibernate, EJB, JNDI) live in the same JAR; the JAR cannot be placed on the Vaadin classpath without breaking Vaadin startup.
+| <<#swing-bridge.interop.sharing-domain-types.type-2, DTO generation>>. _Roadmap._
+|===
+
+Most green-field migrations from a server-backed Swing client fall into Type 3+. The two roadmap strategies are described at the end of this page so you know they're coming, but you should not rely on them yet.
+
+
+[[swing-bridge.interop.sharing-domain-types.type-3]]
+== Type 3 — No Domain Types Cross the Bridge
+
+If every `@ExposedMethod` and listener method uses only JDK types, there's nothing to configure. Codegen handles it; both sides see the same `String`/`int`/`long`. This is the simplest path and a fine starting point for an incremental migration.
+
+[source,java]
+----
+public class MainWindow extends javax.swing.JFrame {
+
+ @ExposedMethod
+ public String getCurrentUserName() {
+ return UserService.current().getDisplayName();
+ }
+
+ @ExposedMethod
+ public int getOpenIssueCount() {
+ return IssueRegistry.openCount();
+ }
+}
+----
+
+
+[[swing-bridge.interop.sharing-domain-types.type-3plus]]
+== Type 3+ — Domain by Reference _(Recommended, Shipped)_
+
+When the Swing application has a thin domain JAR (no embedded ORM startup, no JNDI lookups at construction time), and the Vaadin application starts cleanly with that JAR on its classpath, the cleanest setup is to add it as a Maven dependency on _both_ sides. The same JAR ends up on the Vaadin classpath _and_ inside the Swing app — but they resolve through the same parent classloader, so they yield the same `Class>`.
+
+[[swing-bridge.interop.sharing-domain-types.type-3plus.setup]]
+=== Maven Setup
+
+On the Vaadin app, add the domain JAR as a normal `` (do _not_ put it only in `applibs/`):
+
+[source,xml]
+----
+
+
+
+ com.example
+ example-domain-entities
+ 2.7.0
+
+
+
+ com.vaadin
+ swing-bridge-flow
+ ${swing-bridge.version}
+
+
+----
+
+The Swing application's own JAR — which references the domain types — still goes into `applibs/` as before. At runtime, when the Swing-side classloader loads, say, `com.example.domain.Customer`, parent-first delegation finds it on the Vaadin classpath first.
+
+[[swing-bridge.interop.sharing-domain-types.type-3plus.example]]
+=== End-to-End Example
+
+The Swing side returns entities directly:
+
+[source,java]
+----
+package com.example.swingapp;
+
+import com.example.domain.CityDataBean; // from example-domain-entities.jar
+import com.vaadin.swingbridge.interop.ExposedMethod;
+import com.vaadin.swingbridge.interop.Invocation;
+
+public class MainWindow extends javax.swing.JFrame {
+
+ @ExposedMethod(invocation = Invocation.ASYNC)
+ public CityDataBean[] searchCityData(String query) {
+ return cityService.search(query).toArray(new CityDataBean[0]);
+ }
+}
+----
+
+The generated bridge interface uses the same `CityDataBean` class:
+
+[source,java]
+----
+public interface MainWindowBridge {
+ CompletableFuture searchCityData(String query);
+}
+----
+
+The Vaadin view binds a grid directly to the entity — no DTO, no converter:
+
+[source,java]
+----
+import com.example.domain.CityDataBean;
+import com.example.swingapp.MainWindowBridge;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.swingbridge.SwingBridge;
+import java.util.Arrays;
+
+@Route("cities")
+public class CitiesView extends VerticalLayout {
+
+ private final TextField query = new TextField("Search");
+ private final Grid grid = new Grid<>();
+
+ public CitiesView() {
+ grid.addColumn(CityDataBean::getZipCode).setHeader("Zip");
+ grid.addColumn(CityDataBean::getCity).setHeader("City");
+ add(query, grid);
+
+ query.addValueChangeListener(e -> {
+ String q = e.getValue();
+ SwingBridge.interop().of(MainWindowBridge.class)
+ .requestAsync(b -> b.searchCityData(q))
+ .whenComplete((rows, err) -> getUI().ifPresent(ui -> ui.access(() -> {
+ if (err == null && rows != null) {
+ grid.setItems(Arrays.asList(rows));
+ }
+ })));
+ });
+ }
+}
+----
+
+[[swing-bridge.interop.sharing-domain-types.type-3plus.gotchas]]
+=== Gotchas
+
+*Lazy-loaded Hibernate proxies.* If the Swing side fetches an entity through Hibernate, the entity's collections may be uninitialised proxies tied to a session. Force-load and detach _before_ returning, or the Vaadin side throws `LazyInitializationException` on first access:
+
+[source,java]
+----
+@ExposedMethod(invocation = Invocation.ASYNC)
+public Customer findCustomer(String id) {
+ Customer c = entityManager.find(Customer.class, id);
+ Hibernate.initialize(c.getOrders()); // pre-load lazy collections
+ entityManager.detach(c); // disconnect from the session
+ return c;
+}
+----
+
+*Domain JAR drags in Hibernate.* If the entities JAR pulls Hibernate, JPA providers, or other startup wiring transitively, adding it to the Vaadin classpath may make the Vaadin app fail to start (the persistence provider tries to initialise with no datasource configured). Drop to Type 2 in that case, or split the entities JAR so it only contains the POJOs.
+
+*`javax.persistence` vs `jakarta.persistence`.* Even when the simple names match, `javax.persistence.Entity` and `jakarta.persistence.Entity` are different classes on the JVM. A Vaadin app on Jakarta EE can't import a domain JAR built against `javax.persistence` without conflicts — but the underlying entity bean shape (fields, getters, setters) is identical, so once annotation stripping ships (Type 1), this becomes a one-line build step.
+
+*Domain JAR only in `applibs/`.* This is the most common mistake. A JAR inside `applibs/` is visible only to the Swing-side classloader, not to its parent — so the entity exists on the Swing side but doesn't exist on Vaadin's classpath at all. Either add it as a ``, or move the entities into a JAR that is added as a ``.
+
+
+[[swing-bridge.interop.sharing-domain-types.type-1]]
+== Type 1 — Strip Annotations _(Roadmap)_
+
+For a thin domain JAR whose JPA / EJB / JAXB annotations conflict with the Vaadin app's Jakarta EE stack, the plan is to ship a `strip-annotations` Maven goal that produces a clean variant of the JAR with the offending annotations removed. The original JAR stays in `applibs/` (so Swing-side Hibernate still works); the stripped variant goes on the Vaadin classpath.
+
+[IMPORTANT]
+====
+The `strip-annotations` Mojo currently exists as a code skeleton only — it is not wired into the codegen pipeline, has no integration tests, and is _not production-ready_. Don't depend on it in customer code yet.
+
+In the interim, customers in this profile typically either:
+
+- Repackage the entities JAR by hand (for example with `maven-shade-plugin` + a `relocations` ruleset) to remove `javax.*` annotations, then add the repackaged JAR as a `` and follow the <<#swing-bridge.interop.sharing-domain-types.type-3plus, Type 3+>> recipe; or
+- Treat the application as Type 2 and write hand-rolled DTOs.
+====
+
+When the goal ships, the expected XML will read approximately:
+
+[source,xml]
+----
+
+ com.vaadin
+ swing-bridge-codegen-maven-plugin
+
+
+ strip-domain-annotations
+ strip-annotations
+
+ ${project.basedir}/applibs/example-domain.jar
+ ${project.build.directory}/example-domain-stripped.jar
+
+ javax.persistence
+ javax.ejb
+ javax.xml.bind
+
+
+
+
+
+----
+
+This page will be updated with the final shape once the goal ships.
+
+
+[[swing-bridge.interop.sharing-domain-types.type-2]]
+== Type 2 — DTO Generation _(Roadmap)_
+
+For fat-client Swing applications whose domain JAR cannot be placed on the Vaadin classpath at all (embedded ORM, JNDI lookups at construction, custom serialisers), the plan is a `generate-shared-types` Maven goal that emits clean DTOs from the customer JAR and bridges them with JSON marshalling at the bridge boundary.
+
+[IMPORTANT]
+====
+The `generate-shared-types` Mojo currently exists as a code skeleton only — there is no JSON marshalling integration in the bridge runtime, and the DTO emission path is not wired end-to-end. Don't depend on it in customer code yet.
+
+In the interim, the supported approach is to write DTOs by hand on the Vaadin side and convert at the bridge boundary:
+
+. Define a `CustomerDto` class on the Vaadin side mirroring the fields of `Customer`.
+. Have the Swing-side `@ExposedMethod` accept and return the DTO. The Swing-side method maps to and from the entity locally.
+. The Vaadin side never sees the original entity.
+
+This sidesteps the classloader problem entirely (the DTO lives on the Vaadin classpath, never in `applibs/`), at the cost of a manual mapping layer.
+====
+
+
+[[swing-bridge.interop.sharing-domain-types.summary]]
+== Summary
+
+[cols="1,2,2,2,2", options="header"]
+|===
+| Profile | Vaadin classpath has | `applibs/` has | Conversion | Status
+
+| Type 3
+| Vaadin code only
+| Swing app JAR
+| None
+| Shipped
+
+| Type 3+
+| Vaadin code + domain JAR
+| Swing app JAR
+| None (same `Class>`)
+| Shipped
+
+| Type 1
+| Vaadin code + _stripped_ domain JAR
+| Swing app JAR + _original_ domain JAR
+| None (same shape, different annotations)
+| Roadmap
+
+| Type 2
+| Vaadin code + generated DTOs
+| Fat-client JAR (untouched)
+| JSON marshalling at the bridge
+| Roadmap
+|===
+
+For most customers, the right path today is *Type 3+*. If the domain JAR can't be added to the Vaadin classpath (whether because of annotation conflicts or embedded ORM), fall back to hand-rolled DTOs until Type 1 or Type 2 ships.