VI Shipping

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:

PortUsed by
8000the gateway
8001the readiness probe
8002the introspection UI
8003the configuration UI
8004the gateway’s own readiness probe
8005streamed 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 types generates its types, toa npm installs 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

KeyRequiredMeaning
nameyes1–32 letters and digits, starting with a letter
componentsyesat least one <namespace>.<name>; default.<name> without a namespace
servicesnoextension services to run in this process
replicasnointeger, at least 1; 2 when absent
resourcesnocpu and memory as [request, limit], or null
imagenobase image for the composition’s build

And of evicted: components and services, each a list with at least one entry.

Search
Search the documentation