Skip to content

InMemory Transport

The InMemory Transport is RPC Dart’s highest performance transport, designed for communication within a single process. It provides zero-copy object transfer, making it ideal for high-throughput applications, testing, and scenarios where maximum performance is required.

Zero-Copy Performance

Objects are passed by reference without serialization overhead, providing maximum performance for large data transfers.

Type Safety

Full Dart type information is preserved across RPC boundaries, maintaining compile-time safety.

Perfect for Testing

Ideal for unit and integration tests where you need fast, predictable communication.

Memory Efficient

No data duplication or serialization means minimal memory footprint.

The most significant advantage of InMemory transport is zero-copy object transfer:

class LargeDataSet {
final List<ComplexObject> items = List.generate(
100000,
(i) => ComplexObject(id: i, data: List.filled(1000, i)),
);
}
// With InMemory transport, this entire object is passed by reference
// No serialization, no copying, just a direct reference transfer!
final result = await service.processLargeDataSet(LargeDataSet());

Unlike network transports that require serialization, InMemory transport preserves full Dart type information:

// Complex types work seamlessly
class User {
final String id;
final DateTime createdAt;
final Set<String> permissions;
final Map<String, dynamic> metadata;
User({
required this.id,
required this.createdAt,
required this.permissions,
required this.metadata,
});
}
// Types are preserved exactly as they are
final user = await userService.getUser('123');
// user is exactly User type, not a Map or reconstructed object

InMemory transport works with transport pairs - a connected client and server transport:

// Create a pair of connected transports
final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();
// Setup server endpoint
final responder = RpcResponderEndpoint(transport: serverTransport);
responder.registerServiceContract(CalculatorResponder());
responder.start();
// Setup client endpoint
final caller = RpcCallerEndpoint(transport: clientTransport);
final calculator = CalculatorCaller(caller);
// Make RPC calls
final result = await calculator.add(AddRequest(10, 5));
print('Result: ${result.result}'); // Result: 15

Here’s a complete example showing InMemory transport in action:

calculator_contract.dart
abstract interface class ICalculatorContract {
static const name = 'Calculator';
static const methodAdd = 'add';
static const methodSubtract = 'subtract';
static const methodMultiply = 'multiply';
static const methodDivide = 'divide';
}
class MathRequest {
final double a;
final double b;
MathRequest(this.a, this.b);
}
class MathResponse {
final double result;
MathResponse(this.result);
}

For applications that need maximum performance:

class DataProcessor {
Future<ProcessedData> processLargeDataset(LargeDataset data) async {
// With InMemory transport, the entire dataset is passed by reference
// No serialization overhead for millions of records
return ProcessedData(
results: data.records.map((record) => processRecord(record)).toList(),
);
}
}

InMemory transport is perfect for testing:

void main() {
group('Calculator Service Tests', () {
late RpcResponderEndpoint responder;
late RpcCallerEndpoint caller;
late CalculatorCaller calculator;
setUp(() async {
final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();
responder = RpcResponderEndpoint(transport: serverTransport);
responder.registerServiceContract(CalculatorResponder());
responder.start();
caller = RpcCallerEndpoint(transport: clientTransport);
calculator = CalculatorCaller(caller);
});
tearDown(() async {
await caller.close();
await responder.close();
});
test('should add numbers correctly', () async {
final result = await calculator.add(MathRequest(2, 3));
expect(result.result, equals(5));
});
test('should handle division by zero', () async {
expect(
() => calculator.divide(MathRequest(10, 0)),
throwsA(isA<RpcException>()),
);
});
});
}

For applications where all services run in the same process:

void main() async {
// Create transport pairs for different services
final (userClientTransport, userServerTransport) = RpcInMemoryTransport.pair();
final (orderClientTransport, orderServerTransport) = RpcInMemoryTransport.pair();
final (paymentClientTransport, paymentServerTransport) = RpcInMemoryTransport.pair();
// Setup all service responders
final userResponder = RpcResponderEndpoint(transport: userServerTransport);
userResponder.registerServiceContract(UserServiceResponder());
final orderResponder = RpcResponderEndpoint(transport: orderServerTransport);
orderResponder.registerServiceContract(OrderServiceResponder(
userService: UserServiceCaller(RpcCallerEndpoint(transport: userClientTransport)),
paymentService: PaymentServiceCaller(RpcCallerEndpoint(transport: paymentClientTransport)),
));
final paymentResponder = RpcResponderEndpoint(transport: paymentServerTransport);
paymentResponder.registerServiceContract(PaymentServiceResponder());
// Start all services
await Future.wait([
userResponder.start(),
orderResponder.start(),
paymentResponder.start(),
]);
// Services can now communicate with zero serialization overhead
}

InMemory transport supports all RPC streaming patterns with zero-copy performance:

// Responder
Stream<NumberData> getNumbers(NumberRequest request, {RpcContext? context}) async* {
for (int i = 1; i <= request.count; i++) {
// Each NumberData object is passed by reference
yield NumberData(
value: i,
metadata: {'generated_at': DateTime.now()},
complexData: generateComplexData(i),
);
await Future.delayed(Duration(milliseconds: 100));
}
}
// Caller
Stream<NumberData> getNumbers(NumberRequest request) {
return callServerStream<NumberRequest, NumberData>(
methodName: 'getNumbers',
request: request,
);
}
// Usage
await for (final numberData in calculator.getNumbers(NumberRequest(count: 10))) {
// numberData retains full type information and complex nested objects
print('Number: ${numberData.value}, Complex: ${numberData.complexData}');
}
// Responder
Future<SumResult> sumNumbers(Stream<NumberData> numbers, {RpcContext? context}) async {
double sum = 0;
int count = 0;
await for (final numberData in numbers) {
// Each numberData object comes with full type information
sum += numberData.value;
count++;
}
return SumResult(total: sum, count: count);
}
// Caller
Future<SumResult> sumNumbers(Stream<NumberData> numbers) {
return callClientStream<NumberData, SumResult>(
methodName: 'sumNumbers',
requests: numbers,
);
}
// Responder
Stream<ProcessedData> processStream(Stream<RawData> input, {RpcContext? context}) async* {
await for (final rawData in input) {
// Process the raw data (objects passed by reference)
final processed = ProcessedData(
id: rawData.id,
processedAt: DateTime.now(),
result: processComplexData(rawData),
);
yield processed;
}
}
// Caller
Stream<ProcessedData> processStream(Stream<RawData> input) {
return callBidirectionalStream<RawData, ProcessedData>(
methodName: 'processStream',
requests: input,
);
}

InMemory transport provides exceptional performance:

OperationInMemoryHTTPWebSocket
Simple RPC~0.01ms~5-10ms~2-5ms
Large Object (1MB)~0.01ms~50-100ms~20-40ms
Streaming (1000 items)~5ms~1-2s~0.5-1s
// Large object transfer comparison
class LargeData {
final List<ComplexObject> items = List.generate(10000, (i) => ComplexObject(i));
}
// InMemory transport: ~0 additional memory (reference passing)
// Network transports: ~2x memory usage (original + serialized copy)

InMemory transport only works within a single Dart process:

// ✅ This works - same process
final (client, server) = RpcInMemoryTransport.pair();
// ❌ This doesn't work - different processes
// Cannot share InMemory transport across process boundaries

Cannot be used for distributed systems:

// ❌ Cannot use InMemory for microservices
// Use HTTP, WebSocket, or other network transports instead
// ✅ Use for monolithic applications
final (userClient, userServer) = RpcInMemoryTransport.pair();
final (orderClient, orderServer) = RpcInMemoryTransport.pair();

Objects passed through InMemory transport share references:

class MutableData {
List<String> items = [];
}
// ⚠️ Be careful with mutable objects
final data = MutableData();
data.items.add('initial');
final result = await service.processData(data);
// The original object might be modified by the service
// This is both an advantage (performance) and consideration (side effects)
// Perfect for unit tests
void main() {
group('Service Tests', () {
late ServiceCaller service;
setUp(() async {
final (client, server) = RpcInMemoryTransport.pair();
// Setup endpoints...
service = ServiceCaller(caller);
});
test('fast and reliable tests', () async {
final result = await service.performOperation(data);
expect(result, meets(criteria));
});
});
}
// Ideal for CPU-intensive processing
class ImageProcessor {
Future<ProcessedImage> processImage(RawImageData data) async {
// Large image data passed by reference - no copying overhead
return ProcessedImage(
processed: applyFilters(data),
metadata: generateMetadata(data),
);
}
}
// Use InMemory for internal services, network transports for external
class HybridApplication {
void setup() {
// Internal high-performance communication
final (cacheClient, cacheServer) = RpcInMemoryTransport.pair();
setupCacheService(cacheServer);
// External API communication
final httpTransport = RpcHttpTransport(url: 'https://api.external.com');
setupExternalApiClient(httpTransport);
}
}
try {
final result = await service.processData(data);
return result;
} on RpcException catch (e) {
// Handle RPC-specific errors
logger.error('RPC Error: ${e.code} - ${e.message}');
return defaultResult;
} catch (e) {
// Handle other errors
logger.error('Unexpected error: $e');
rethrow;
}
// Before: Direct method calls
class Calculator {
double add(double a, double b) => a + b;
}
final calc = Calculator();
final result = calc.add(10, 5);
// After: RPC with InMemory transport
abstract interface class ICalculatorContract {
static const name = 'Calculator';
static const methodAdd = 'add';
}
// Setup once
final (client, server) = RpcInMemoryTransport.pair();
// ... setup endpoints ...
// Use anywhere
final result = await calculator.add(MathRequest(10, 5));

When you need to scale beyond a single process:

// Development with InMemory
final (client, server) = RpcInMemoryTransport.pair();
// Production with HTTP
final httpTransport = RpcHttpTransport(url: 'https://api.production.com');
// Same service code works with both!
final calculator = CalculatorCaller(RpcCallerEndpoint(transport: httpTransport));

The InMemory transport is the perfect choice when you need maximum performance within a single process, whether for testing, development, or high-performance monolithic applications. Its zero-copy nature makes it unmatched for scenarios where serialization overhead would be a bottleneck.