How We Built Multi-Language SDKs for Prism, a Payment Library, Using FFI
Prism is a payment library designed to handle the full payment lifecycle — authorize, capture, refund, and void — across multiple…
How We Built Multi-Language SDKs for Prism, a Payment Library, Using FFI

Prism is a payment library designed to handle the full payment lifecycle — authorize, capture, refund, and void — across multiple connectors like Stripe, Adyen, and others.
While the core is written in Rust, we needed SDKs that work seamlessly in Node.js, Python, and Java. The challenge was straightforward: how do we share business logic across languages without rewriting it multiple times?
The Design Choice: gRPC vs FFI
We initially evaluated two approaches:
- HTTP/gRPC-based communication
- Foreign Function Interface (FFI)
gRPC worked, but it came with trade-offs:
- Running a sidecar service
- Additional network hops
- Extra operational overhead
For something running on the same machine, this felt unnecessary.
FFI, on the other hand, allowed:
- In-process execution
- Zero network overhead
- No additional runtime to manage
That made it the clear choice.
A Simple Pattern Behind Every Handler
Before exposing anything over FFI, we broke down how Prism handlers actually work.
Every operation — whether authorize, capture, refund, or void — follows the same structure:
fn authorize(req) {
// 1. Transform input → connector-specific request
// 2. Make HTTP call → send to connector
// 3. Transform response → normalize output
}
In essence: transform → call → transform
This realization was key.
Instead of exposing entire handlers over FFI, we only needed to expose the transformation logic:
- Request transformation
- Response transformation
The HTTP call stays within the SDK, written in the target language.
This keeps:
- The FFI surface minimal
- Responsibilities cleanly separated
- Future extensions predictable
Using UniFFI
To expose Rust functions across languages, we used UniFFI.
With simple annotations, UniFFI generates bindings and compiles platform-specific binaries (.so, .dylib, .dll) that can be loaded from other languages.
Example:
#[derive(uniffi)]
fn authorize_req_transformer(...) { ... }
#[derive(uniffi)]
fn authorize_res_transformer(...) { ... }
These same functions are reused across:
- HTTP handlers
- gRPC handlers
- FFI-based SDKs
One implementation, multiple entry points.
How the SDK Uses FFI
Each SDK client corresponds to a service — for example, PaymentClient for payment operations.
The compiled Rust binary is loaded once, and FFI calls are made as part of each request:
export class PaymentClient {
async authorize(msg) {
const req = ffi.authorize_req_transformer(msg);
const result = await fetch(req);
const res = ffi.authorize_res_transformer(result);
return res;
}
}
Loading the binary is straightforward:
const ffi = require('./libprism_ffi.so'); // .dylib on macOS, .dll on Windows
From the SDK’s perspective, these functions behave like local calls.
Crossing the Language Boundary
One challenge with FFI is data transfer — JSON doesn’t map cleanly across language boundaries.
We solved this using Protobuf as the interchange format.
Flow:
User SDK FFI Rust
│ │ │ │
│ JSON │ │ │
│────────────▶│ │ │
│ │ Protobuf │ │
│ │ Bytes │ │
│ │───────────────▶│ │
│ │ │ Bytes │
│ │ │─────────────▶│
│ │ │ Process │
│ │ │◀─────────────│
│ │◀───────────────│ │
│◀────────────│ │ │
Why Protobuf?
- Efficient binary format
- Strong typing across languages
- Shared schema via
.protofiles
This ensures type safety in Node, Python, and Java — all from a single source of truth.
What the Developer Sees
All of this complexity is hidden from the end user.
After installing the SDK:
import { PaymentClient, types } from 'hyperswitch-prism';
const paymentClient = new PaymentClient(connectorConfig);
const response = await paymentClient.authorize(authorizeRequest);
if (response.status === types.PaymentStatus.CHARGED) {
console.log("Payment successful");
}
That’s it.
No awareness of:
- Rust internals
- FFI boundaries
- Binary loading
- Serialization formats
Just a clean, simple API.
Final Thoughts
By narrowing the FFI surface to just transformation logic, we achieved:
- Maximum code reuse
- Minimal overhead
- Clean separation of concerns
Rust handles business logic. SDKs handle I/O. Protobuf ensures consistency.
The result is a scalable, multi-language SDK architecture without duplication or unnecessary complexity.
The full implementation is available here:
메타데이터
- post_id
- a5dd06f2c9c6
- slug
- how-we-built-multi-language-sdks-for-prism-a-payment-library-using-ffi-a5dd06f2c9c6
- url
- https://medium.com/@explorejeeva/how-we-built-multi-language-sdks-for-prism-a-payment-library-using-ffi-a5dd06f2c9c6
- canonical_url
- https://medium.com/@explorejeeva/how-we-built-multi-language-sdks-for-prism-a-payment-library-using-ffi-a5dd06f2c9c6
- author_url
- https://medium.com/@explorejeeva
- status
- ok
- fetched_at
- 2026-06-29 01:02:39