Decide What Runs Together
Locally, all the shop’s components run as one process: app, one of the two that npm start keeps under PM2, is toa compose with every
directory under components. A deployment needs an answer: do orders and wallets share a
process, how many copies run, how much memory may each take? The answer is a list of compositions in the Context. A composition is a group of
components that run as one process, and it is what gets built, deployed and replicated.
Components know nothing of it. Compositions says what
the decision weighs.
§Declaring nothing
The Context toa create wrote has no compositions:
# context.toa.yaml
name: store
registry: registry.example.com/store
# … Every component then gets a composition of its own: store.orders becomes store-orders, billing.wallets becomes billing-wallets, accounts, which has no namespace, becomes default-accounts, and
a deployment runs one process per component, two replicas each.
This is a valid place to start.
§Grouping components
# context.toa.yaml
compositions:
- name: core
components:
- store.orders
- billing.wallets store.orders and billing.wallets now run in one process, and are deployed, scaled and
restarted together.
They share connections. A process opens one connection to the broker, one client to each database and one to Redis, whatever number of its components use them. Count connections by compositions and replicas: twenty components as twenty compositions of two replicas hold forty of each, and as one composition hold two. Group components when the broker or the database reports too many connections.
- A component is named
<namespace>.<name>. One without a namespace is listed by its name:accounts. - A composition name is 1 to 32 letters and digits, starting with a letter. No hyphens.
- A component no composition lists still gets its own.
Mistakes are refused by name:
Composition 'core' lists an unknown component 'store.order'. An unknown key inside a composition is refused too.
To run everything as one process, list every component in one composition.
§Replicas
compositions:
- name: core
components: [store.orders, billing.wallets]
replicas: 3 Without replicas, a composition deploys two. A composition you did not declare also deploys
two; to change that, declare it.
Any replica may take any call. Nothing an operation keeps in process memory is visible to the next call.
§Resources
Every deployed workload states what CPU and memory it may take. State a default for the whole Context, and override it where a composition differs:
resources:
cpu: [200m, 1]
memory: [256Mi, 1Gi]
compositions:
- name: core
components: [store.orders, billing.wallets]
resources:
cpu: [500m, 2]
memory: [512Mi, 2Gi] Each pair is [request, limit], in Kubernetes quantities. A replica is scheduled where the
request is free. It is throttled at the CPU limit, and killed and restarted when it passes the
memory limit. The numbers are for the whole process: components of one composition are not
limited apart from each other.
There is no built-in default. A deployment that finds none is refused:
Composition 'core' declares no resources. Declare them on it or as the context's 'resources',
or 'resources: null' to deploy it without any. resources: null, on a composition or on the Context, deploys without requests and limits.
The memory limit also sizes the heap. A workload with a memory limit runs Node with --max-old-space-size at three quarters of it. If you set NODE_OPTIONS yourself, yours is
kept.
Resources are read only when deploying. A local run ignores them.
§Services in a composition
Extensions such as the gateway run a process of their own, and by default each is deployed separately. To run fewer pods, a composition can run a service inside its own process:
compositions:
- name: edge
components: [store.orders]
services:
- exposition A service is named by its shortcut (exposition, configuration, introspection, cadence) or
by package name (@toa.io/extensions.exposition).
- The service no longer has a deployment of its own. It runs in the pods of
edge. - Its address does not change: same name, same ingress.
- Several compositions may list the same service. All their pods serve it.
- A service that no composition lists is deployed on its own.
- Listing a service brings its extension in, even when no component declares it.
An extension that has no service cannot be listed:
Composition 'edge' lists '@toa.io/extensions.telemetry', which contributes no service. §Ports
Services in one composition must not claim the same port; two different compositions may. These ports are taken:
| Port | Used by |
|---|---|
8000 | the gateway |
8001 | the readiness probe |
8002 | the introspection UI |
8003 | the configuration UI |
8004 | the gateway’s own readiness probe |
8005 | streamed calls to a component |
A conflict is refused when deploying, naming both claimants. A service of your own extension picks a port not in this table.
§Running a composition with services locally
toa compose takes the same list with --service, repeated:
$ npx toa compose ./components/* --service exposition --service configuration The list is exact: only the named services start. If the gateway needs the configuration values
service, and that service is not running elsewhere, name it too. Without --service, the list is
read from the TOA_SERVICES environment variable, separated by spaces.
The generated application keeps the services apart instead: its sys process is toa serve configuration exposition introspection, as ecosystem.config.js says.
§Leaving things out
pictures resizes images and runs on special hardware that another team operates. It
stays part of the application, but this deployment must not run it. Such a part is evicted:
compositions:
- name: core
components: [store.orders, billing.wallets, pictures]
services: [exposition]
evicted:
components:
- pictures
services:
- exposition An evicted component stays part of the application:
- other components call it by name and receive its events;
- events it receives are published;
toa typesgenerates its types,toa npminstalls what it declares, and it is in the component map;- its configuration is declared in the Context like any other.
But the deployment does not run it: it is in no pod and no image, and its migrations are not applied. Anything only evicted components needed is not deployed either.
An evicted service is deployed nowhere, alone or inside a composition.
- A composition whose components are all evicted is not deployed.
- Naming a service that would not have been deployed anyway changes nothing.
- An unknown component is refused:
'evicted' names an unknown component 'default.picture'.
§Running an evicted component yourself
Whatever runs the component is responsible for three things.
Give it its variables. toa env skips evicted components. Ask for the component by name:
$ npx toa env production -c pictures --as .env.pictures Give it its secrets. Its configuration values are still served by the deployment; only the secrets are yours to supply.
Build it from the same sources the Context was deployed from. See An evicted component.
Until something runs it, a call to an evicted component waits.
§Evicting in one environment
evicted@production:
components:
- pictures Locally the component runs with everything else. In production it is left out.
§One process for everything
toa mono runs every component and every service their extensions need in one process, with no
list to maintain. Locally it takes the place of sys and app, so stop them first:
$ npm stop
$ npx toa mono ./components/* The same shape can be deployed: toa deploy --mono builds one image that runs the whole
application as a single workload. Its replicas and resources come from mono:
mono:
replicas: 2
resources:
cpu: [200m, 2]
memory: [256Mi, 2Gi] Without mono, it deploys two replicas with the Context’s resources. compositions are
ignored in this form; evicted still applies. See Deploy.
Moving between one process and many is a change to the Context only.
§Base image
A component may ask for its own base image, as Build Images shows. All members of a composition are built into one image, so where they disagree, the composition decides:
compositions:
- name: media
image: node:24.22.1-alpine3.24
components: [pictures, store.catalog] Otherwise the build is refused:
Composition 'media' requires different base images for its components. Specify base image for
the composition in the context. §How to group
Start with whatever is simplest to operate, and split when you have a reason:
- Components that call each other constantly are cheaper together.
- A component whose failure must not take others down gets its own composition.
- A component with a very different load is scaled separately.
- A component holding sensitive secrets can be isolated so that only its pods receive them.
- Long-running work competes with short work in the same process.
The introspection topology shows which components actually talk to each other.
§Every key of a composition
| Key | Required | Meaning |
|---|---|---|
name | yes | 1–32 letters and digits, starting with a letter |
components | yes | at least one <namespace>.<name>; default.<name> without a namespace |
services | no | extension services to run in this process |
replicas | no | integer, at least 1; 2 when absent |
resources | no | cpu and memory as [request, limit], or null |
image | no | base image for the composition’s build |
And of evicted: components and services, each a list with at least one entry.