A Better Way for Inter-Service Communication
The Common Approach: The Ball Game
Imagine building a system where different services pass a ball around, incrementing its value each time. Here's how it typically looks:
// Node.js service
import Redis from 'redis';
const redis = Redis.createClient();
await redis.connect();
// Subscribe to receive the ball
redis.subscribe('ball:node', async (message) => {
try {
const data = JSON.parse(message);
if (typeof data.value !== 'number') {
console.error('Invalid ball value');
return;
}
const newValue = data.value + 1;
await redis.publish('ball:python', JSON.stringify({ value: newValue }));
} catch (e) {
console.error('Failed to parse ball data', e);
}
});
# Python service
import redis
import json
r = redis.Redis()
pubsub = r.pubsub()
def handle_message(message):
try:
data = json.loads(message['data'])
if not isinstance(data.get('value'), (int, float)):
print('Invalid ball value')
return
new_value = data['value'] + 1
r.publish('ball:rust', json.dumps({'value': new_value}))
except json.JSONDecodeError:
print('Failed to parse ball data')
except KeyError:
print('Missing value in ball data')
pubsub.subscribe('ball:python')
for message in pubsub.listen():
if message['type'] == 'message':
handle_message(message)
The problems here are clear:
- Manual JSON parsing and error handling everywhere
- No type safety between services
- Channel names need to be manually synchronized
- Different error handling patterns per language
- Easy to make mistakes when modifying the data structure
A Better Way: Schema-First Development with Memorix
First, let's set up Memorix:
# Install CLI
brew tap uvop/memorix
brew install memorix
# Install client for your language
npx jsr add @memorix/client-redis # Node.js/Bun/Deno
pip install memorix-client-redis # Python
cargo add memorix-client-redis # Rust
Now, define your schema once (schema.memorix):
Config {
export: {
engine: Redis(env(REDIS_URL))
files: [
{
language: typescript
path: "node/src/schema.generated.ts"
}
{
language: python
path: "python/src/schema_generated.py"
}
{
language: rust
path: "rust/src/schema_generated.rs"
}
]
}
}
Enum {
System {
NODE
PYTHON
RUST
}
}
Task {
pass_ball: {
key: System
payload: u64
}
}
Generate the type-safe clients:
memorix codegen ./schema.memorix
Look how clean the services become:
// Node service
import * as mx from "./schema.generated.ts";
const memorix = new mx.Memorix();
await memorix.connect();
const gen = await memorix.task.pass_ball.dequeue(mx.System.NODE);
for await (const ball of gen.asyncIterator) {
const biggerBall = ball + 1;
await memorix.task.pass_ball.enqueue(mx.System.PYTHON, biggerBall);
}
# Python service
from schema_generated import Memorix, System
memorix = Memorix()
memorix.connect()
for ball in memorix.task.pass_ball.dequeue(System.PYTHON):
bigger_ball = ball + 1
memorix.task.pass_ball.enqueue(System.RUST, bigger_ball)
The benefits are immediately clear:
- No manual JSON parsing or error handling
- Full type safety and IDE autocomplete in every language
- Consistent API across languages and platforms (Redis, Kafka, etc.)
- Modern language features like async generators
- Compile-time errors catch mistakes early
- Changed from publish/subscribe to queue-based messaging for easy scaling across multiple instances
Other Memorix features
Rich Data Typing:
Task {
processUser: {
payload: { # Inline type
id: u64
authType: UserType # Enums
avatar_url: string? # Optional
data: UserData # Sharing complex typing
}
}
}
Enum {
UserAuthType {
EMAIL
GOOGLE
GITHUB
}
}
Type {
UserData: {
is_admin: boolean
preferences: [
{
preference_id: string
value: boolean
}
]
}
}
Caching with TTL:
Cache {
userSession: {
key: string
payload: UserSession
ttl: "3600" # Expires in 1 hour
}
}
Pub/Sub for Broadcast Messages:
PubSub {
userLoggedIn: {
payload: {
userId: string
timestamp: u64
}
}
}
Task Queues with Different Strategies:
Task {
processImage: {
payload: ImageData
queue_type: "lifo" # Last in, first out
}
}
Scalability with Ease:
- Import/export schema files
- Private/public APIs
- Namespacing
- Environment variables support
- Cross-service type sharing
Conclusion
The ball game example shows how Memorix transforms messy inter-service communication into clean, type-safe code. Instead of worrying about JSON parsing and message formats, you can focus on building features.
Ready to try it? Check out the complete example on GitHub or dive into the documentation.
Top comments (3)
I canβt wait for systems to start adopting this kind of concept, loved the Rust touch π How much work is it to support more languages?
Actually not that much, since the compiler is already built for it (also in Rust π¦).
I'm in the process of adding support for Golang.
Maybe C# next?
Exactly the languages I had in mind π
Sounds great, Ill spread the word