Architecture
Strata organizes services around a simple hierarchy: a service contains contexts, and each context exposes operations. Communication flows through a pluggable backend, and scaling happens through service groups.
Service Structure
A Strata application has three levels:
StrataService
The top-level object. It holds configuration, manages the backend connection, coordinates contexts, and runs global middleware. You create one per process.
StrataContext
A logical grouping of related operations -- analogous to an Express Router. A users context might handle get, create, and delete. Contexts are registered with the service under a name, and incoming requests are routed to the matching context.
Operations
The individual units of work within a context. Each operation is an async function that receives a request and returns a response (or calls request.succeed() / request.fail() directly). This is where your business logic lives.
import { StrataService, StrataContext } from '@strata-js/strata';
// Create the service
const service = new StrataService({
service: { serviceGroup: 'UserService' },
backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } },
});
// Create a context
const users = new StrataContext('users');
// Register operations
users.registerOperation('get', async (request) =>
{
const { userId } = request.payload;
const user = await db.findUser(userId);
return { user };
});
users.registerOperation('create', async (request) =>
{
const user = await db.createUser(request.payload);
return { user };
});
// Register the context with the service
service.registerContext(users);
// Start listening for requests
await service.start();That's it. The service listens on its queue, routes incoming requests to the users context, and dispatches to the matching operation.
Pluggable Backends
The backend is the transport layer. It handles sending and receiving messages, managing queues, and coordinating service discovery. Critically, your service code never touches the backend directly -- it's configured once and forgotten.
Built-in Backends
| Backend | Transport | Use Case |
|---|---|---|
redis | Redis lists | Simple, reliable, well-understood |
redis-streams | Redis Streams | Most tested, consumer group support |
null | Nothing | Unit testing |
Swapping Backends
Changing the backend is a configuration change. Your contexts, operations, and middleware don't change at all:
// Redis lists
const service = new StrataService({
service: { serviceGroup: 'MyService' },
backend: { type: 'redis', redis: { host: 'localhost' } },
});
// Redis Streams -- same service code, different config
const service = new StrataService({
service: { serviceGroup: 'MyService' },
backend: { type: 'redis-streams', redis: { host: 'localhost' } },
});Custom Backends
You can implement your own backend by extending BaseStrataBackend and registering it with the service. As long as your backend can send and receive JSON messages, it works. See the Backends reference for built-in options, or the Writing a Custom Backend guide.
Client / Service Model
Strata has two sides: services that process requests and clients that send them.
- A
StrataServicelistens on a queue for incoming requests, routes them through middleware and contexts, and sends responses back. - A
StrataClientsends requests to a service group's queue and listens for responses on its own dedicated queue.
A service can also have an embedded client for making calls to other service groups. This is how services communicate with each other -- service A's client sends a request to service B's queue.
import { StrataClient } from '@strata-js/strata';
const client = new StrataClient({
client: { name: 'MyApp' },
backend: { type: 'redis', redis: { host: 'localhost' } },
});
await client.start();
// Send a request to the UserService
const response = await client.request('UserService', 'users', 'get', {
userId: '12345',
});Service Groups
A service group is one or more instances of the same service, all reading from the same request queue. This is how Strata handles horizontal scaling.
When you start three instances of UserService, they all listen on the Requests:UserService queue. Each incoming request goes to exactly one instance -- whichever pops it first. No load balancer, no coordinator, no configuration.
/-> UserService (instance 1)
Client -> RPUSH Requests:UserService <envelope> ----------> UserService (instance 2)
\-> UserService (instance 3)Each instance has its own unique ID but shares the service group name. The backend handles the distribution transparently. To scale up, start another instance. To scale down, stop one. The queue absorbs the difference.
Layered Architecture (Optional)
Strata itself doesn't enforce how you organize code inside your operations. However, the framework was designed with iDesign principles in mind, and the recommended architecture follows a layered pattern:
| Layer | Responsibility | Example |
|---|---|---|
| Clients | External consumers (contexts/operations) | Route requests to managers |
| Managers | Business logic orchestration | Coordinate engines and RA |
| Engines | Pure business logic, no I/O | Validation, transformation |
| Resource Access | Data persistence, external APIs | Database queries, HTTP calls |
| Utils | Cross-cutting concerns | Logging, config, helpers |
Each layer only calls the layer directly below it. Managers coordinate between engines and resource access. Engines are pure functions -- no I/O, no state, easily testable. This isn't required, but it works well with Strata's structure.