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 .
SOCIAL SHARE CARD GENERATOR