Table of Contents generated with DocToc
FOAM’s nano-service architecture enables fine-grained services that can be co-located or distributed without code changes. This document provides a comprehensive guide to understanding and building nano-services.
FOAM’s nano-service architecture solves a fundamental problem in distributed systems: how do you write code that works identically whether services are local (same JVM/process) or remote (across a network)?
The answer is location agnostic design through three mechanisms:
┌─────────────────────────────────────────────────────────────────┐
│ Application Code │
│ │
│ imports: ['myAIService'] │
│ ... │
│ this.myAIService.chat(x, request) // Same API everywhere │
│ │
└──────────────────────────┬──────────────────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Local │ │ HTTP │ │ WebSocket│
│ (same │ │ (REST) │ │ (real- │
│ JVM) │ │ │ │ time) │
└──────────┘ └──────────┘ └──────────┘
The calling code never changes. The transport is a configuration detail.
Services are accessed through dependency injection via the context system:
foam.CLASS({
name: 'MyFeature',
imports: [
'calculatorService', // Could be local or remote
'storageService' // Application doesn't know or care
],
methods: [
async function computeTotal(items) {
// This call works identically whether the service is:
// - In the same JVM (zero network overhead)
// - On another server via HTTP
// - On another server via WebSocket
var result = await this.calculatorService.sum(this.__subContext__, items);
return result;
}
]
});
A nano-service is defined by a FOAM Interface with two special flags:
foam.INTERFACE({
package: 'com.example',
name: 'CalculatorService',
skeleton: true, // Generate server-side message handler
client: true, // Generate client-side proxy
methods: [
{
name: 'sum',
async: true,
type: 'Double',
args: [
{ name: 'x', type: 'Context' },
{ name: 'values', type: 'Double[]' }
]
}
]
});
From this single interface definition, FOAM generates:
CalculatorServiceSkeleton - Server-side class that receives messages and calls the real implementationClientCalculatorService - Client-side proxy that marshals method calls into network requestsA Box is FOAM’s fundamental transport abstraction. It has exactly one method:
foam.INTERFACE({
package: 'foam.box',
name: 'Box',
methods: [
{
name: 'send',
type: 'Void',
args: [
{ name: 'envelope', type: 'foam.box.Envelope' }
]
}
]
});
That’s it. A Box can only send. This simplicity is intentional - it’s the minimal interface needed to abstract any transport mechanism.
Messages are wrapped in an Envelope that carries the payload and a reply destination:
foam.CLASS({
package: 'foam.box',
name: 'Envelope',
properties: [
{
class: 'Object',
name: 'message' // The actual payload (RPC call, response, etc.)
},
{
class: 'FObjectProperty',
name: 'replyBox', // Where to send the response
of: 'foam.box.Box'
}
]
});
These three pieces form a layered message-passing system:
┌─────────────────────────────────────────────────────────┐
│ Box │
│ - The transport interface │
│ - Has one method: send(envelope) │
│ - Knows HOW to deliver (HTTP, WebSocket, local, etc.) │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Envelope │ │
│ │ - The container/wrapper │ │
│ │ - Contains: message + replyBox │ │
│ │ - replyBox is another Box for the response │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ RPCMessage │ │ │
│ │ │ - The actual method call │ │ │
│ │ │ - Contains: name (method) + args │ │ │
│ │ │ - Example: { name: "sum", args: [x, [1,2]] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The flow:
This separation allows:
Different Box classes handle different transports:
| Box Class | Transport | Use Case |
|---|---|---|
HTTPBox |
HTTP POST | Standard request/response |
WebSocketBox |
WebSocket | Real-time, bidirectional |
RawWebSocketBox |
Raw WebSocket | Custom protocols |
SessionClientBox |
Decorator | Adds session ID to requests |
TimeoutBox |
Decorator | Adds request timeouts |
RetryBox |
Decorator | Automatic retry on failure |
NullBox |
No-op | Testing, fire-and-forget |
Boxes compose like DAOs - wrap one with another to add behavior:
// Client-side box chain:
{
"class": "foam.box.SessionClientBox", // Add session authentication
"delegate": {
"class": "foam.box.TimeoutBox", // Add 30-second timeout
"timeout": 30000,
"delegate": {
"class": "foam.box.HTTPBox", // Actual transport
"url": "service/calculatorService"
}
}
}
The service never knows about these layers. The Box interface is the only contract.
The Stub/Skeleton pattern makes network boundaries invisible:
┌──────────────────┐ ┌──────────────────┐
│ CLIENT │ │ SERVER │
│ │ │ │
│ Application │ │ Service Impl │
│ │ │ │ ▲ │
│ ▼ │ │ │ │
│ ClientStub │ │ Skeleton │
│ (generated) │ │ (generated) │
│ │ │ Network │ ▲ │
│ ▼ │ │ │ │
│ Box ─────────┼────────────────────┼─────► Box │
│ │ │ │
└──────────────────┘ └──────────────────┘
calculatorService.sum(x, values))RPCMessage with method name and argumentsMethod calls are serialized as RPCMessage objects:
foam.CLASS({
package: 'foam.box',
name: 'RPCMessage',
properties: [
{ name: 'name' }, // Method name: "chat"
{ name: 'args' } // Arguments: [context, chatRequest]
]
});
When you set skeleton: true and client: true on an interface, FOAM generates:
ClientCalculatorService (client-side stub), generated by foam.INTERFACE (src/foam/lang/Interface.js):
foam.CLASS({
package: 'com.example',
name: 'ClientCalculatorService',
implements: ['com.example.CalculatorService'],
properties: [
{
class: 'Stub',
of: 'com.example.CalculatorService',
name: 'delegate'
}
]
});
The Stub property (src/foam/lang/Stub.js) installs one method per interface method. Each call wraps its name and arguments in an RPCMessage, sends it in an Envelope through the delegate Box, and resolves with the data of the RPCReturnMessage that comes back on the reply box.
CalculatorServiceSkeleton (server-side handler):
// Generated - do not edit
package com.example;
public class CalculatorServiceSkeleton extends foam.box.AbstractSkeleton {
protected foam.lang.XFactory delegateFactory_;
public void send(foam.box.Envelope envelope) {
if ( ! ( envelope.getMessage() instanceof foam.box.RPCMessage) ) {
return;
}
foam.box.RPCMessage rpc = (foam.box.RPCMessage) envelope.getMessage();
foam.lang.X x = envelope.getX();
Object result = null;
try {
switch ( rpc.getName() ) {
case "sum":
result = ((CalculatorService) (getDelegateFactory().create(x))).sum(
x,
(Double[]) (rpc.getArgs() != null && rpc.getArgs().length > 1 ? rpc.getArgs()[1] : null));
break;
default: throw new RuntimeException("Method not found.");
}
} catch (Throwable t) {
envelope.replyWithException(t);
return;
}
if ( envelope.getReplyBox() != null ) {
foam.box.RPCReturnMessage reply = (foam.box.RPCReturnMessage)
getX().create(foam.box.RPCReturnMessage.class);
if ( result != null ) reply.setData(result);
envelope.getReplyBox().send(new foam.box.Envelope(reply, null));
}
}
}
Define the data structures your service will use:
foam.CLASS({
package: 'com.example',
name: 'SearchRequest',
properties: [
{ class: 'String', name: 'query' },
{ class: 'Int', name: 'maxResults', value: 10 }
]
});
foam.CLASS({
package: 'com.example',
name: 'SearchResult',
properties: [
{ class: 'String', name: 'title' },
{ class: 'String', name: 'snippet' },
{ class: 'Double', name: 'score' }
]
});
foam.CLASS({
package: 'com.example',
name: 'SearchResponse',
properties: [
{
class: 'FObjectArray',
name: 'results',
of: 'com.example.SearchResult'
}
]
});
foam.INTERFACE({
package: 'com.example',
name: 'SearchService',
skeleton: true, // Generate skeleton
client: true, // Generate client stub
documentation: 'Service for performing searches',
methods: [
{
name: 'search',
documentation: 'Search for documents matching the query',
async: true,
type: 'com.example.SearchResponse',
args: [
{ name: 'x', type: 'Context' },
{ name: 'request', type: 'com.example.SearchRequest' }
]
}
]
});
foam.CLASS({
package: 'com.example',
name: 'ServerSearchService',
implements: ['com.example.SearchService'],
javaImports: [
'java.util.ArrayList',
'java.util.List'
],
properties: [
{
class: 'String',
name: 'indexPath',
documentation: 'Path to the search index'
}
],
methods: [
{
name: 'search',
javaCode: `
List<SearchResult> results = new ArrayList<>();
// Your search implementation here
// ...
SearchResponse response = new SearchResponse();
response.setResults(results.toArray(new SearchResult[0]));
return response;
`
}
]
});
Create a services.jrl file:
p({
"class": "foam.core.boot.CSpec",
"name": "searchService",
"serve": true,
"authenticate": true,
"boxClass": "com.example.SearchServiceSkeleton",
"serviceScript": """
return new com.example.ServerSearchService.Builder(x)
.setIndexPath("/var/search/index")
.build();
""",
"client": """
{
"class": "com.example.ClientSearchService",
"delegate": {
"class": "foam.box.SessionClientBox",
"delegate": {
"class": "foam.box.HTTPBox",
"url": "service/searchService"
}
}
}
"""
})
foam.CLASS({
name: 'SearchView',
extends: 'foam.u2.View',
imports: ['searchService'],
requires: ['com.example.SearchRequest'],
methods: [
async function doSearch(query) {
var request = this.SearchRequest.create({ query: query });
var response = await this.searchService.search(this.__subContext__, request);
return response.results;
}
]
});
| Property | Purpose |
|---|---|
name |
Service name (how it’s accessed in context) |
serve |
Whether to expose via network |
authenticate |
Require valid session |
boxClass |
The skeleton class name |
serviceScript |
Server-side instantiation code |
client |
Client-side configuration |
lazy |
Create on first access (default: true) |
The client block defines what gets created on the client side:
"client": """
{
"class": "com.example.ClientCalculatorService",
"delegate": {
"class": "foam.box.SessionClientBox",
"delegate": {
"class": "foam.box.HTTPBox",
"url": "service/calculatorService"
}
}
}
"""
This creates a client stub wrapping a session-aware HTTP box.
Here’s a complete example showing all the pieces together for a notification service:
foam.CLASS({
package: 'com.example',
name: 'NotifyRequest',
properties: [
{ class: 'String', name: 'userId' },
{ class: 'String', name: 'message' },
{ class: 'String', name: 'channel', value: 'email' } // email, sms, push
]
});
foam.CLASS({
package: 'com.example',
name: 'NotifyResponse',
properties: [
{ class: 'Boolean', name: 'success' },
{ class: 'String', name: 'messageId' }
]
});
foam.INTERFACE({
package: 'com.example',
name: 'NotificationService',
skeleton: true,
client: true,
documentation: 'Service for sending notifications across channels',
methods: [
{
name: 'notify',
async: true,
type: 'com.example.NotifyResponse',
args: [
{ name: 'x', type: 'Context' },
{ name: 'request', type: 'com.example.NotifyRequest' }
]
}
]
});
foam.CLASS({
package: 'com.example',
name: 'ServerNotificationService',
implements: ['com.example.NotificationService'],
methods: [
{
name: 'notify',
javaCode: `
// Route to appropriate channel handler
String channel = request.getChannel();
boolean success = false;
String messageId = java.util.UUID.randomUUID().toString();
if ( "email".equals(channel) ) {
// Send email...
success = true;
} else if ( "sms".equals(channel) ) {
// Send SMS...
success = true;
}
NotifyResponse response = new NotifyResponse();
response.setSuccess(success);
response.setMessageId(messageId);
return response;
`
}
]
});
p({
"class": "foam.core.boot.CSpec",
"name": "notificationService",
"serve": true,
"authenticate": true,
"boxClass": "com.example.NotificationServiceSkeleton",
"serviceScript": """
return new com.example.ServerNotificationService(x);
""",
"client": """
{
"class": "com.example.ClientNotificationService",
"delegate": {
"class": "foam.box.SessionClientBox",
"delegate": {
"class": "foam.box.HTTPBox",
"url": "service/notificationService"
}
}
}
"""
})
foam.CLASS({
name: 'AlertManager',
imports: ['notificationService'],
requires: ['com.example.NotifyRequest'],
methods: [
async function sendAlert(userId, message) {
var request = this.NotifyRequest.create({
userId: userId,
message: message,
channel: 'email'
});
var response = await this.notificationService.notify(
this.__subContext__,
request
);
return response.success;
}
]
});
When services run in the same JVM, there’s no serialization, no network round-trip. The “Box” is just a direct method call.
Move a service to another server by changing the CSpec. No application code changes required.
HTTP, WebSocket, or raw sockets - the Box abstraction supports all of them interchangeably.
Services can be arbitrarily small because deployment overhead is minimal. Multiple nano-services can share a single server process.
Add authentication, timeouts, retries, logging by wrapping Boxes - no code changes to the service itself.
Request and response types are FOAM models - automatically validated, serialized, and documented.
FOAM nano-services provide:
The key insight: services are objects, not endpoints. The transport is configuration, not API design.