In-process vs out-of-process plugins: the design fork that shaped my Windows app A developer building Chauffeur, a local Windows tool for snapshotting and restoring desktop state, deliberately implemented two plugin models: in-process C++ shared libraries for performance and out-of-process add-ins for crash isolation. The choice between in-process and out-of-process effectively determines the language and safety trade-offs, with the REST API serving as both integration and version boundary. I'm building Chauffeur, a local Windows tool that snapshots your whole working state across apps — open files, editor tabs, browser tabs, projects — and restores it on demand. "Save game" for your desktop. 100% local: no cloud, no account, no telemetry. But this post isn't a pitch. It's about the one architecture decision that shaped everything: how do other apps plug into the core? I ended up running two completely different plugin models on purpose, and the reasoning is worth sharing. The setup A small background service C++/Qt 6 exposes a local REST API. Everything integrates through that single point. The question was never really "what language" — it was in-process or out-of-process. Those are the two real choices, and the language question mostly answers itself afterward. Model 1 — In-process plugins C++ shared libs These are C++ shared libraries the core loads directly into its own address space, behind a plain extern "C" boundary: // Stable C ABI — no name mangling, no STL across the boundary extern "C" { declspec dllexport int chauffeur plugin init const ChauffeurHostApi host ; declspec dllexport void chauffeur plugin shutdown ; } Pros: fast, share memory with the core, zero IPC overhead. Perfect for trusted, first-party, performance-sensitive work. Cons, and they're real: Model 2 — Out-of-process add-ins separate processes The Excel/Word add-ins C VSTO , the browser extensions JS , the VS Code extension — none of them share memory with the core. They register over the local REST API and send a heartbeat: POST /register { "id": "excel", "type": "push", ... } POST /plugin/alive/excel // heartbeat If one crashes, hangs, or gets killed, the core just sees a dead socket and moves on. The blast radius is one process. Pros: crash isolation for free, no shared address space, no ABI headaches — and the language becomes irrelevant. C , JS, Python, anything that speaks HTTP. Cons: IPC latency, serialization, and you're now running N processes. The lesson The thing that surprised me: the in-process/out-of-process choice is the language choice in disguise. Picking per integration instead of forcing one model everywhere is the decision I'd make again without hesitation. The REST boundary doubles as the version boundary too — you negotiate a protocol version on /register instead of versioning a memory layout. On exception safety If you go in-process, try/catch around every interface call helps with well-behaved exceptions — but it does nothing against segfaults, stack smashes, or infinite loops. Those take the host with them. If you need real isolation, a process boundary is the only thing that actually gives it to you. Versioned interfaces IPluginV1/IPluginV2 solve ABI compatibility, not crash safety. Don't conflate the two.