2. Getting Started¶
Guide Home | Previous: 1. Overview | Next: 3. Core Concepts
View in another language — C#/.NET · C++ · Java · Kotlin · Node/TypeScript
The document that owns this chapter's contract — none. This is a walkthrough for installing and confirming your first working setup.
First install the package and run a minimal example of two processes calling each other (§1-§2), then follow the actual TicTacToe sample through the flow of creating one room (§3-§11).
1. Installation¶
Get it from Maven Central. The minimal combination needed to build one server consists of these two artifacts.
// The contract and runtime
implementation("systems.zlink:zlink-framework-core")
// DI/lifecycle registration
implementation("systems.zlink:zlink-framework-spring-boot-starter")
Artifacts to add when you need them:
| Artifact | When to add it |
|---|---|
zlink-framework-locations-redis |
When using the Redis location store for auto-connect (10-location) |
zlink-framework-codec-protobuf · -codec-msgpack |
To use instead of the default JSON codec (05-channel-messaging §7) |
zlink-stream-connector |
When building an external client (a game client, mobile) (09-stream) |
zlink-http-client |
When the server calls out over HTTP (HTTP Client guide) |
JDK 21 or later is required.
The license differs by layer — core/binding is MPL-2.0, framework is FSL-1.1-ALv2, and
zlink-http-client is Apache-2.0. There's no cost to building and selling a service
(17-alternative §7).
2. A Minimal Example — Two Processes Calling Each Other¶
With no location store and no Redis, try one request/reply over a manual connection with the endpoint specified directly. This confirms that installation is complete.
The shared contract. Both processes reference the same record.
The server process. Owns the greeting channel and registers a handler.
@EnableZLinkFramework
@SpringBootApplication
public class ServerApplication {
@Bean
ZLinkFrameworkConfigurer zlink() {
return options -> {
// Finds handler types.
options.addHandlersFromPackageOf(ServerApplication.class);
// Names the mesh.
ZLinkMeshNodeBuilder mesh = options.addRouteMesh("services")
// Its own endpoint for other processes to connect to.
.listen("tcp://0.0.0.0:7101");
// This process handles "greeting".
mesh.channelName("greeting").server()
.addRequestHandler(HelloHandler.class, Hello.class, Greeting.class);
};
}
}
// A handler that processes one request.
public final class HelloHandler implements ZLinkRequestHandler<Hello, Greeting> {
@Override
public CompletionStage<Greeting> handle(Hello request, ZLinkMessageContext context) {
return CompletableFuture.completedFuture(new Greeting("hello, " + request.name()));
}
}
The client process. Joins the same mesh and calls greeting.
@Bean
ZLinkFrameworkConfigurer zlink() {
return options -> {
// It also needs its own endpoint.
ZLinkMeshNodeBuilder mesh = options.addRouteMesh("services").listen("tcp://0.0.0.0:7102");
// The call-only side is Client.
mesh.channelName("greeting").client();
// Manual connection — write the server endpoint directly.
mesh.peerConnections().connect("tcp://127.0.0.1:7101");
};
}
@RestController
class HelloController {
private final ZLinkRouteClient route;
HelloController(ZLinkRouteClient route) { this.route = route; }
@GetMapping("/hello/{name}")
CompletionStage<String> hello(@PathVariable String name) {
// The target is just one ChannelName. Which node handles it isn't specified.
return route.requestToChannel("greeting", new Hello(name))
.submit(Greeting.class)
.thenApply(Greeting::text);
}
}
Start the server first, then the client, and call curl http://localhost:5000/hello/world
— it returns hello, world.
This confirms three things: the package is wired up, the two processes are connected through
the mesh, and the call was routed by logical name (greeting) alone. This example
has no Redis and no location store. For the calling code to stay the same as servers scale
up and down, you need auto-connect, which is covered by
10-location.
3. TicTacToe — The Flow of Creating One Room¶
From here on, we'll use an actual sample. The API server doesn't pick a specific Play node
— it only passes the room's stable type and its initial settings. The Framework selects one
of the Object Servers that registered that type, and issues a globally unique SpotId.
3.1 Execution Flow¶
The API code never carries the Play node's NodeRid or endpoint. The same creation code is
used even as Play nodes are added or replaced.
3.2 Sample Locations¶
| What to check | File |
|---|---|
| Full run | samples/java/TicTacToe/run_sample.sh |
| API entry point | samples/java/TicTacToe/Server/.../api/ApiServer.java |
| Play entry point | samples/java/TicTacToe/Server/.../play/PlayServer.java |
| HTTP handler | samples/java/TicTacToe/Server/.../api/handlers/CreateGameHttpHandler.java |
| Game Spot | samples/java/TicTacToe/Server/.../play/infrastructure/zlink/spots/tictactoegamespot/TicTacToeGame.java |
| Shared messages | samples/java/TicTacToe/Shared/.../contracts/Messages.java |
The table's relative paths are rooted at framework/languages/java.
4. API Server Configuration¶
The API server registers a Location Store and an Object Client role. The Object Client role is used to create or call Actors and Spots on another Object Server.
return options -> {
// Registers a shared Store so every process queries the same location information.
options.addLocationStore(new ZLinkRedisLocationStore(
settings.redisEndpoint(), settings.redisKeyPrefix()));
ZLinkMeshNodeBuilder mesh = options.addRouteMesh(SampleNodes.MESH)
.listen(settings.meshEndpoint())
.setRoutingIdPrefix("tictactoe-api");
// The API process doesn't hold any Object — it only initiates remote Object calls.
mesh.objects().client();
};
The sample reads the peer endpoint from a config file for reproducible local runs. This endpoint only sets up the connection — it doesn't specify which Play node the new Game Spot gets placed on.
5. Creating a Spot from an HTTP Request¶
The HTTP handler uses the spot manager it received through DI.
// The HTTP handler uses the ZLinkSpotManager it received through DI.
@PostMapping("/games")
public CompletionStage<CreateGameHttpRes> create(@RequestBody CreateGameHttpReq request) {
String gameName = (request.gameName() == null || request.gameName().isBlank())
? SampleDefaults.GAME_NAME
: request.gameName();
return spots
// A node that provides this stable type becomes a candidate.
.create(SampleTypes.GAME_SPOT)
// Selects the RouteMesh to create the Object on.
.inMesh(SampleNodes.MESH)
.request(new TicTacToeGameCreateReq(
gameName,
// The initial settings passed to the new Spot's onCreate.
SampleDefaults.REQUIRED_LEVEL))
.submit()
.thenApply(created -> new CreateGameHttpRes(
// Uses the Framework-issued SpotId as the room id.
created.spot().spotId(),
settings.playEndpoints(),
settings.playNodes(),
gameName,
SampleDefaults.REQUIRED_LEVEL));
}
Use create to create a new User Spot where the caller doesn't decide the SpotId. To
look up or create the same SpotId again, use GetOrCreate(spotId, spotType).
6. Registering a Stable Type on the Play Server¶
The Framework considers only Serving Object Servers that have registered the requested
stable type as creation candidates. The Play server registers the TicTacToeGame factory
as follows.
ZLinkMeshNodeBuilder mesh = options.addRouteMesh(SampleNodes.MESH)
.listen(settings.meshEndpoint())
.setRoutingIdPrefix("tictactoe-play");
mesh.objects().server()
.addSpotFactory(
// The same stable type the API passed to create.
SampleTypes.GAME_SPOT,
TicTacToeGame.class,
factory -> factory.disableRelocation());
The sample does not define a contract for preferring a specific Play node or placing by
NodeRid. The Framework and Location Store decide the placement candidate and capacity.
7. Validating the Initial Settings¶
The selected Play node creates the Spot, then hands the initial request to onCreate. The
Spot validates the settings and returns whether it accepts creation.
@Override
public CompletionStage<ZLinkSpotCreateResponse> onCreate(ZLinkMessage request) {
TicTacToeGameCreateReq settings = request.decode(TicTacToeGameCreateReq.class);
if (settings.gameName() == null || settings.gameName().isBlank()) {
return CompletableFuture.completedFuture(
ZLinkSpotCreateResponse.reject("GameName is required."));
}
this.gameName = settings.gameName();
this.requiredLevel = settings.requiredLevel();
// Only after accept is this Spot published as Ready in the Location Store.
return CompletableFuture.completedFuture(ZLinkSpotCreateResponse.accept());
}
If creation is rejected, that reservation is never published as a Ready Spot. The caller receives a typed failure as the completion result.
8. What the ClientServer Channel Is For¶
TicTacToe's tictactoe.api ClientServer channel is used when a Play session requests user
authentication from the API server. It isn't used for Game Spot creation.
// API process: handles the authentication request.
options.addClientServerChannel(SampleChannels.API)
.server()
.listen()
.addRequestHandler(
AuthenticatePlayerHandler.class,
AuthenticatePlayerReq.class,
AuthenticatePlayerRes.class);
// Play process: sends the authentication request.
options.addClientServerChannel(SampleChannels.API).client();
Object creation and a ClientServer call are different features. No dedicated
room-creation channel or CreateGameHandler is added.
9. Build and Run¶
# Build the sample first.
./gradlew -p framework/languages/java/samples :java:TicTacToe:build
# Prepares Redis and 4 processes, and verifies the whole scenario.
framework/languages/java/samples/java/TicTacToe/run_sample.sh
The runner runs 2 APIs and 2 Plays. After creating a Game Spot, it verifies that participants connected to different Play endpoints join the same room, then checks game messaging and end-of-game cleanup.
10. What to Check When It Fails¶
| Symptom | What to check |
|---|---|
| No creation candidate | Check whether the Play process registered an Object Server and the GameSpot stable type on the same MeshName. |
| Startup fails | Check the Redis connection, MeshName, listen endpoint, and any duplicate-registration error. |
| Creation is rejected | Check the initial settings onCreate received and the reject reason. |
| The client can't join the room | Check whether the HTTP response's RoomId was passed to the Actor join request as-is. |
The next chapters each explain the role of the channel, Spot, Actor, Stream, and Location Store used here.