← Back to list

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…

Explorejeeva · 2026-05-03 14:01 · 1 claps · 2.6 min read
#payments #prism #hyperswitch #payment-library
Open on Medium ↗
Wiki topics: FIN · Fintech & Banking LIT · Literature & Writing 📚 · Books & Reading

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 .proto files

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:

https://github.com/juspay/hyperswitch-prism


메타데이터
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