Skip to content

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.

typescript
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

BackendTransportUse Case
redisRedis listsSimple, reliable, well-understood
redis-streamsRedis StreamsMost tested, consumer group support
nullNothingUnit testing

Swapping Backends

Changing the backend is a configuration change. Your contexts, operations, and middleware don't change at all:

typescript
// 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 StrataService listens on a queue for incoming requests, routes them through middleware and contexts, and sends responses back.
  • A StrataClient sends 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.

typescript
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:

LayerResponsibilityExample
ClientsExternal consumers (contexts/operations)Route requests to managers
ManagersBusiness logic orchestrationCoordinate engines and RA
EnginesPure business logic, no I/OValidation, transformation
Resource AccessData persistence, external APIsDatabase queries, HTTP calls
UtilsCross-cutting concernsLogging, 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.