-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
This guide walks you through setting up Vyra, writing your first typed request/response interaction, and broadcasting events.
- Java 17 or later: Vyra uses records and modern concurrency primitives.
Note
To follow along without installing a broker, use the vyra-inmemory transport. Everything in this guide runs in a single JVM.
Vyra is modular: a typical project needs three modules:
-
vyra-core, the messaging bus abstractions. - A transport, such as
vyra-inmemoryfor tests and local dev,vyra-redisfor distributed deployments. - A serializer, such as
vyra-jackson(JSON, Smile, CBOR) orvyra-gson(JSON).
This guide uses the in-memory transport and Jackson.
dependencies {
// Core framework abstractions
implementation("com.marioded.vyra:vyra-core:0.1.0")
// Transport (choose one)
implementation("com.marioded.vyra:vyra-inmemory:0.1.0") // Local dev
// implementation("com.marioded.vyra:vyra-redis:0.1.0") // Distributed Redis transport
// Serializer (choose one)
implementation("com.marioded.vyra:vyra-jackson:0.1.0") // JSON through Jackson (also Smile, CBOR)
// implementation("com.marioded.vyra:vyra-gson:0.1.0") // JSON through Gson
}<dependencies>
<!-- Core framework abstractions -->
<dependency>
<groupId>com.marioded.vyra</groupId>
<artifactId>vyra-core</artifactId>
<version>0.1.0</version>
</dependency>
<!-- Transport (choose one) -->
<dependency>
<groupId>com.marioded.vyra</groupId>
<artifactId>vyra-inmemory</artifactId>
<version>0.1.0</version>
</dependency>
<!-- <dependency>
<groupId>com.marioded.vyra</groupId>
<artifactId>vyra-redis</artifactId>
<version>0.1.0</version>
</dependency> -->
<!-- Serializer (choose one) -->
<dependency>
<groupId>com.marioded.vyra</groupId>
<artifactId>vyra-jackson</artifactId>
<version>0.1.0</version>
</dependency>
<!-- <dependency>
<groupId>com.marioded.vyra</groupId>
<artifactId>vyra-gson</artifactId>
<version>0.1.0</version>
</dependency> -->
</dependencies>Important
A serializer is required. Vyra does not bundle a default serializer so that vyra-core stays dependency-free. See Serialization for details.
Network messages are plain, immutable Java records:
package com.example.usingvyra;
// 1. A request to fetch player information
public record GetPlayerRequest(String playerId) {}
// 2. The response returned by the backend
public record PlayerResponse(String playerId, String name, int score) {}
// 3. A one-way notification event broadcast to all subscribers
public record PlayerJoinedEvent(String playerId, long timestamp) {}This example uses two Vyra instances: one for the backend service, one for the client. Each instance needs a transport and a serializer.
Tip
For local development, use the InMemoryVyra factory. To connect multiple instances in the same JVM, share an InMemoryBus.
package com.example.usingvyra;
import com.marioded.vyra.core.Vyra;
import com.marioded.vyra.inmemory.InMemoryVyra;
import com.marioded.vyra.inmemory.InMemoryBus;
import com.marioded.vyra.jackson.JacksonSerializer;
// A shared bus represents the local network. In production this is your broker.
InMemoryBus sharedBus = new InMemoryBus();
// Server node
Vyra backend = InMemoryVyra.inMemory(sharedBus)
.serializer(JacksonSerializer.jackson())
.nodeId("backend-service")
.build();
// Client node
Vyra client = InMemoryVyra.inMemory(sharedBus)
.serializer(JacksonSerializer.jackson())
.nodeId("api-gateway")
.build();Before sending or handling messages, map each class to a stable string identifier. This identifier travels over the wire instead of Java class names:
backend.register("player.get", GetPlayerRequest.class);
backend.register("player.info", PlayerResponse.class);
backend.register("player.joined", PlayerJoinedEvent.class);
client.register("player.get", GetPlayerRequest.class);
client.register("player.info", PlayerResponse.class);
client.register("player.joined", PlayerJoinedEvent.class);Tip
Stable identifiers decouple your wire protocol from Java package names and class refactoring.
The backend handles incoming requests; the client sends them.
sequenceDiagram
autonumber
actor Caller as Client Application
participant Client as Vyra Client
participant Bus as Transport (InMemory / Redis)
participant Backend as Vyra Backend
actor Handler as Service Handler
Caller->>Client: request("backend-service", req, PlayerResponse.class, 3s)
Client->>Bus: send(REQUEST, destination: "backend-service")
Bus->>Backend: deliver to worker queue
Backend->>Handler: invoke handler on thread pool
Handler-->>Backend: CompletableFuture<PlayerResponse>
Backend->>Bus: send(RESPONSE, destination: clientNodeId)
Bus->>Client: deliver to response listener
Client-->>Caller: complete CompletionStage<PlayerResponse>
handle(...) registers your business logic and immediately starts listening for requests:
import java.util.concurrent.CompletableFuture;
backend.handle("backend-service", GetPlayerRequest.class, req -> {
System.out.println("Processing request for player: " + req.playerId());
// Handlers return a CompletionStage, enabling async database or API integrations
PlayerResponse response = new PlayerResponse(req.playerId(), "Mario", 1500);
return CompletableFuture.completedFuture(response);
});request(...) returns a standard Java CompletionStage and requires an explicit timeout:
import java.time.Duration;
client.request("backend-service", new GetPlayerRequest("p-100"), PlayerResponse.class, Duration.ofSeconds(3))
.thenAccept(player -> {
System.out.println("Player found: " + player.name() + " (Score: " + player.score() + ")");
})
.exceptionally(throwable -> {
System.err.println("Request failed: " + throwable.getMessage());
return null;
});Important
responseType must be a concrete Class<T>. To return a collection such as List<PlayerResponse>, wrap it in a record (e.g. record PlayerList(List<PlayerResponse> players) {}).
Tip
Need to query all instances of a service to locate a resource? Use client.broadcastRequest("backend-service", ...) instead of client.request(...). See Request / response for broadcast routing and the silent non-owner pattern.
Events are fire-and-forget broadcasts delivered to all active subscribers on a channel:
// 1. Subscribe on backend (or multiple microservices)
backend.subscribe("game-events", PlayerJoinedEvent.class, event -> {
System.out.println("Broadcast received: " + event.playerId() + " joined at " + event.timestamp());
});
// 2. Publish from client
client.publish("game-events", new PlayerJoinedEvent("p-100", System.currentTimeMillis()));When your application stops, call close() to release transport connections and cancel pending timeouts:
client.close();
backend.close();Switching from the in-memory transport to a distributed broker changes only the factory configuration. With Redis, for example:
import io.lettuce.core.RedisClient;
import com.marioded.vyra.redis.RedisVyra;
import com.marioded.vyra.jackson.JacksonSerializer;
// 1. Create your RedisClient
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
// 2. Build the Vyra instance with the redis transport
Vyra vyra = RedisVyra.redis(redisClient)
.serializer(JacksonSerializer.jackson())
.nodeId("backend-node-1")
.build();Your message definitions, handlers and requests remain unchanged.
- Request / response to learn about worker queue load balancing, broadcast requests, timeouts.
- Events to learn at-most-once delivery.
- Serialization to choose between JSON, Smile and CBOR.
- Timeouts & errors for error recovery.
Vyra documentation