Skip to content

3. Core Concepts

Guide Home | Previous: Kotlin Quickstart — from Install to a First Request | Next: Channel Messaging

What you get from this chapter

You can tell what a channel, a Spot, an Actor, STREAM and Location each are, and when to choose them. The code in this chapter comes from the language-specific example repositories.

The ZLink framework provides channel · spot · actor · stream · location as its core concepts. Every other chapter is a variation on these. We'll go through them in order below, and cover relocation — an actor or spot moving to another node partway through — along the way. Later dedicated chapters cover how to implement and operate each concept in practice.

1. channel — a connection between servers

MeshNode is the basic unit of a connection between servers. A single MeshNode can take on two independent roles.

  • Object role — the place where spots and actors are placed. Covered separately in spot and actor.
  • Channel role — the place where request/send/publish are exchanged. Covered in this document.

ChannelName is a logical name that groups together the nodes in that mesh sharing the same function — instead of an address (host:port), you pick a call target by a name like "orders". The caller passes only the ChannelName to the route client — MeshName is fixed at registration and never appears in the call arguments.

The caller doesn't need to know which node is currently handling that request. Pass only a logical name (ChannelName, a spot id, or an actor id) — not an address or node number — and the framework finds the target and delivers the request wherever that name currently lives. This property — the caller doesn't need to know where the target is — is called location transparency. Channels, spots, and actors all work this way. That's why the calling code stays the same whether you scale servers out or in.

When a message is sent by ChannelName, the framework selects one of the nodes currently able to receive the request and delivers it there — this selection is called select-one.

↗ View larger

If three nodes own the same orders channel, one of them is selected per call. The caller doesn't know — and doesn't need to know — which node was selected.

Here's what it looks like to configure both roles on one MeshNode.

The blocks below are the tutorial source as-is — mesh registration and the Object role come from the server side, and the channel's call-only (client) role comes from the client side, so the mesh and channel names are the tutorial's own "game"/"profile", not "orders"/"billing".

// Both sides must name the mesh identically, or they never see each other
// as peers. The routing id names this node; without it the Framework
// assigns a generated one, which a caller cannot type into a URL.
val mesh =
    options
        .addRouteMesh("game")
        .listen("tcp://127.0.0.1:7601")
        // The advertised endpoint must match the endpoint passed to
        // connect(routingId, endpoint). Both use 127.0.0.1 here.
        .setAdvertiseHost("127.0.0.1")
        .setRoutingId(RoutingId.from("game-server-1"))
// A mesh node picks this role once. Keep the builder and reuse it,
// because calling objects().server() a second time is rejected at
// startup.
val objects = mesh.objects().server()
// Only handlers exposed here can be called by other nodes. A handler class
// sitting in the same package but in another group stays unreachable.
mesh.channelName("profile").server().addHandlerGroup(HandlerGroups.PROFILE)
// This node opens an endpoint too. Both sides listen to become peers.
val mesh = options.addRouteMesh("game").listen("tcp://127.0.0.1:7602")

// client() means this node exposes no handler for the channel; it only calls.
mesh.channelName("profile").client()

// A mesh peer connection, not a channel one. The mesh picks a node that
// serves the channel from among the peers it learns this way, so a channel
// call never names a node.
//
// The routing id form states which node is expected at that endpoint. The
// peer is rejected unless it answers with that id AND advertises exactly
// this endpoint string -- which is why the server sets its advertise host.
// connect(endpoint) alone would connect to whoever is there.
mesh.peerConnections().connect(RoutingId.from("game-server-1"), "tcp://127.0.0.1:7601")

Automatic connection management, which avoids hard-coding peer addresses and tracks servers as they scale up or down, is covered by Location.

Note: MeshName and ChannelName are different names. You can register several ChannelNames on one mesh, and the same ChannelName can be used across different meshes.

Several registration types use the name "channel"; they differ in whether they share a socket.

Kind Socket
route mesh channel Shares the already-open MeshNode socket
ClientServer channel Opens its own socket, separate from the MeshNode
fanout channel Opens its own dedicated PUB/SUB socket

pub/sub also splits two ways. Logical Multicast, exchanged between Spots over a route mesh channel, uses the mesh socket as-is, and a fanout channel delivers over its own socket to the connected subscribers with a matching topic subscription. The structural comparison of the three and how to use them is covered by Channel Messaging.

2. spot — a unit that owns state and processes it in order

Many domains — a game room, a guild, or an auction item — have state that several requests may access at the same time. Building this yourself means both finding the process that currently holds the state and routing requests there, while also preventing concurrent access by incoming requests. Keep the state in process memory and you have to manage that routing yourself; keep it in a DB or Redis and you have to read, write, and lock on every request.

A spot lets the framework handle both concerns. It keeps the target as a single in-memory object and processes incoming requests serially, one at a time. Since two requests can never touch the same state at the same time, no lock is needed.

Unlike a channel, a spot is addressed by id. Send to the "orders" channel and any node capable of doing that work handles it. Send a request to a spot id like "room-42", though, and the node where that spot is located receives the message and hands it to that spot to process. The framework finds that node through the same location transparency seen earlier.

↗ View larger

A spot is registered on the MeshNode's Object role. It's a separate surface from the same MeshNode's Channel role.

Spots fall into three kinds — Entry Spot · User Spot · Instance Spot — based on when they're created, and execution mode determines which work runs concurrently. The differences between the three kinds, choosing an execution mode, and registration/lifecycle/timer/outbound are covered by Spot.

3. actor — a state object identified by ID

An actor is a stateful object identified by an ID. A message that arrives with the same ID is always handled by the same instance. An actor always belongs to some spot, and how it binds to an external client connection continues in the next section.

↗ View larger

See Actor and Actor Membership for details.

4. stream — external client connections

A stream is a connection-oriented, bidirectional channel to an external client such as a mobile app or game client. Unlike a server-to-server channel, the server manages connection lifecycle and heartbeats, and one connection maps to a server-side session object. Reconnecting after a disconnect is the client connector's job.

When you bind a session to an actor, the session stops handling messages that arrive over that connection itself and relays them to the bound actor instead. The reverse direction works the same way — a push from the actor goes out to the client through the session bound to that actor.

↗ View larger

So the node that accepts the connection and the node that runs domain logic can be split. Even if the session lives on a gateway node and the actor lives on a different node, the framework keeps the relay path alive. Even if the actor moves via relocation, the same session remains connected to the actor at its new location.

See STREAM for details; how to bind a session to an actor is covered by Session and Actor.

5. relocation — moving to another node

Relocation is when an actor or spot leaves its current owner node and keeps running on another node. It starts from two distinct triggers.

An actor belongs to a spot, and a spot belongs to a node. Relocation preserves this relationship exactly as it is — only the node it runs on changes.

When an actor joins a spot on another node. If an actor requests to join a User Spot that lives on a different node, the moment the join is accepted the actor moves to that node carrying its state and pending work along with it. This is a move the application triggers by request.

↗ View larger

The only thing the join call specifies is the spot id room-42 — there's no argument that names a target node. The framework looks up which node currently holds that spot in the location store and moves actor P to that node. So the names "node A" and "node B" never appear anywhere in the application code. Once the move finishes, actor P belongs to node B's room-42 spot and follows the same execution rules as Q and R.

When an operator relocates a host for maintenance or deployment without downtime. The operator moves the spots and actors on one host to another host. The framework handles this without individual join requests from the application, and once it's done, the original host can be shut down.

↗ View larger

A server holding state can't simply be taken down, so maintenance or deployment usually means dropping the connection and making clients wait. Host Relocate preserves the spot's and actor's state while moving them to another node, freeing up node A. The caller still sends requests to the same id, room-42, so it never needs to know the move happened. The net effect is that a stateful service can be replaced with zero downtime, the way a stateless service would be.

Both paths follow the same relocation policy. What happens to application state during the move (not moved, recreated, or restored as-is) is fixed at spot/actor factory registration and doesn't change while running.

Policy types and selection criteria are covered by Relocation; making an actor join call and receiving the completion result are covered by Actor Membership; and Host Relocate as zero-downtime maintenance/deployment, along with how a relocation unit is scoped, is covered by 12-operations §2.

6. location — address resolution

Application code uses only logical names like a channel name, and the actual peer address (host:port) is resolved by a location store shared across the whole deployment. Each server registers its own location as a descriptor in the store on startup, and the calling side looks up the target in the store by logical name and connects. When the server topology changes, connections update accordingly.

Usage is covered by Location.

Manual connections — specifying an endpoint directly at registration without a store — are also supported, for development/testing and small fixed deployments (How Channels Work). You can't use both approaches together on the same MeshNode.

See it in a sample — TicTacToe. This is the smallest example where all of these concepts show up in one sample. They all meet in one place: the Play server's registration code.

Concept In TicTacToe
channel The Play server looks up credentials over an independent tictactoe.api ClientServer Channel
spot One match is one TicTacToeGame spot — both players' moves are processed serially inside it
actor A player is an actor, and even after a reconnect the same actor picks up the ongoing match
stream The client connects directly to the Play STREAM endpoint from the API response to make moves and receive pushes
location The Redis location store automatically picks which Play node will create a new TicTacToeGame spot — the API code has no specific Play node address

You've seen what problem each concept solves above; this sample shows what it looks like when they're put together.

7. Where to Start for What You're Trying to Do

Once the concepts are clear, the next question is, "So which surface do I use?" There are eight starting points, all obtained from DI or the current handler context. The application never picks a transport socket or endpoint directly.

What you're trying to do Starting surface What you specify
Send directly to a node, or by channel name route client Node-direct is MeshName + target RID; channel is ChannelName
Send to a Spot spot client Global Spot ID
Send to an Actor actor client Global Actor ID
Create or find a User Spot spot manager Spot type, and a global Spot ID if needed
Create or find an Actor actor manager Global Actor ID and Actor type
Publish a Logical Multicast spot publisher client ChannelName and topic
Publish classic pub/sub fanout client fanout ChannelName, and topic if needed
Send to or reply to a STREAM client session client The current session

The exact type names are language-specific and are defined in the 13. Interface Catalog chapter.

Completion has two consistent forms. A send-family call finishes with no return value once the send slot accepts it, and a request-family call finishes with one of reply · timeout · route error. This holds no matter which surface you use (Backpressure Visible in the API).

8. Naming Convention

Uses the Java notation as-is. Any surface the Kotlin layer adds follows the same rule.

  • Public types use the ZLink prefix (capital L).
  • A coroutine extension is a suspend function, or returns a flow.
  • The Maven coordinates and package are systems.zlink.*.
  • The underlying zlink core C API is zlink_* snake_case.

9. What the Framework Owns and What It Doesn't

Everything below is handled internally by the framework. None of it appears in application code.

Owned
Transport address selection and peer reconnect
Multipart framing and packet codec
Reply correlation
Backpressure queue

These, by contrast, are outside this framework's contract scope. They're handled by the external edge.

Not owned
External client authentication and quota
WAF
Public API versioning
Billing

Server-to-server communication and a real-time state server are this framework's territory. Policy for an edge exposed directly to the internet is owned by whatever sits in front of it.