Contracts
For one component to call another, it has to know something about it: which operations exist, what input each accepts, what it answers. That knowledge is the callee’s contract.
In a system of independently deployed processes, how a caller comes to know a contract, and which version of it, shapes how the system starts, fails, and upgrades.
§A contract is given, not asked for
A common approach is discovery: a process starts, finds the services it depends on, and asks each what it offers. This makes startup depend on other processes being present and makes the order of starting matter.
Toa takes the opposite approach. A component is given the contracts of the components it may call. They are taken from the application’s own sources when it is built or deployed, and supplied to every process as a map of the whole application.
Three properties follow.
A process starts on its own. Starting makes no calls to other components. A composition is ready when its own connections are up, whatever else is or is not running. There is no startup order to arrange and no chain of processes waiting on one another.
A call needs no lookup. A call is addressed by name, and the name is sufficient to send it. The contract is consulted to validate the request.
The application is described once. The map is derived from the manifests. It cannot disagree with them, because it is not maintained separately.
§Versions
Each component in the map has a version, derived from the content of its sources. The version changes when the component changes and at no other time. It is not a number someone remembers to increase.
A process checks that the components it runs match the versions the map states. A process running sources the map does not describe is refused at startup. This catches a deployment assembled from mismatched parts before it serves anything.
§While two versions serve
Replacing a running component takes time, and during that time both versions serve. They take calls from the same queue. A call is not routed by version; whichever replica takes it serves it.
This has a direct consequence for how contracts may change.
The caller validates. A request is checked against the contract the caller holds, and the callee does not check it again. During a rollout, a request validated against the new contract may be served by an old replica, and the reverse.
So a change to an operation’s input must be compatible across the two versions. Adding an optional property is compatible. Making a property required, renaming it, or changing its meaning is not, since replicas of one version would receive requests shaped for the other.
The same holds for state. Replicas of both versions read and write the same entities. A change to the entity schema has to be one the previous release can live with for the duration of the rollout.
New operations and events are safe. An endpoint only the new version declares is served only by the new version: an old replica that is handed a call to it says so, and the call is sent again until a new replica takes it.
These are the rules of any rolling upgrade. The map and the versions make them concrete: the runtime states which contract a caller holds, and that is the one its code was written against.
§Changes that cannot be rolled
Some changes are incompatible by nature: a renamed property, a field that changes meaning. No ordering of a rolling update makes them safe, because the two versions cannot serve side by side.
For these, the deployment is stopped, upgraded, and started, so that the two versions never serve together. Halt provides a way to do this without losing work in progress.
§Events from outside
A component that receives events from another system has no contract to be given, since the source is not part of the application. Such a receiver states for itself where the events come from, and nothing checks the source’s side.
§Why this matters
Contracts are where the logical architecture meets deployment. Components are independent in code and in process, and they still have to agree. Supplying each process with a consistent description of the whole application keeps that agreement from becoming a runtime negotiation, with the startup dependencies and partial failures such negotiation brings.