Lightweight messaging infrastructure for request/response communication and event-driven architectures.
Vyra turns a message broker into a request/response and event bus. You define your wire contract as plain Java records, register handlers on named service targets, and call them from anywhere in the cluster with an explicit timeout. No code generation, no annotations, no runtime reflection.
// Worker: answer requests on the "game-service" target
vyra.handle("game-service", GetPlayerRequest.class,
req -> CompletableFuture.completedFuture(new PlayerResponse(req.playerId(), "Mario", 1200)));
// Client: async request with a mandatory timeout
vyra.request("game-service", new GetPlayerRequest("p-100"), PlayerResponse.class, Duration.ofSeconds(3))
.thenAccept(player -> System.out.println(player.name() + " has " + player.score() + " points"));The transport only moves bytes. Routing, correlation, timeouts and type conversion all live in the core. You can swap the broker and your code does not change.
- Why Vyra
- Quick start
- Messaging patterns
- Modules
- Documentation
- Building from source
- Contributing
- License
Most Java services that talk to each other end up hand-rolling one of these: a REST client with retries, a JMS wrapper, a custom socket protocol. Vyra gives you a simple, type-safe, asynchronous request/response and event bus with a small API and a clear concurrency model.
- Typed contracts: requests, responses and events are plain Java records. No code generation, no annotation processing.
- Async by default: every operation returns a standard
CompletionStage. Nothing blocks. - Explicit timeouts: every request carries one. A request either completes or fails with
VyraTimeoutExceptionso it never hangs forever. - Worker load balancing: requests to a service target are distributed across all instances listening on it. Add a worker, get more throughput.
- Broadcast queries: ask every instance of a service at once; the first non-null answer wins, silent nodes stay silent.
- Small API: just eight methods on
Vyra, five options on the builder. You can read the whole framework in an hour.
Add the modules you need: core, one transport, one serializer.
dependencies {
implementation("com.marioded.vyra:vyra-core:0.1.0")
implementation("com.marioded.vyra:vyra-redis:0.1.0") // or vyra-inmemory for local dev
implementation("com.marioded.vyra:vyra-jackson:0.1.0") // or vyra-gson
}A serializer is required: vyra-core deliberately ships without one so it stays dependency-free. All nodes in a network
must use the same serializer.
The in-memory transport runs the whole thing in a single JVM, no Redis needed:
record GetPlayerRequest(String playerId) {}
record PlayerResponse(String playerId, String name, int score) {}
// In production this is your broker (for example Redis).
InMemoryBus bus = new InMemoryBus();
Vyra backend = InMemoryVyra.inMemory(bus).serializer(JacksonSerializer.jackson()).build();
Vyra client = InMemoryVyra.inMemory(bus).serializer(JacksonSerializer.jackson()).build();
// Stable wire identifiers — these travel on the wire, not class names
backend.register("player.get", GetPlayerRequest.class);
backend.register("player.info", PlayerResponse.class);
client.register("player.get", GetPlayerRequest.class);
client.register("player.info", PlayerResponse.class);
// handle() registers the handler and starts listening
backend.handle("game-service", GetPlayerRequest.class, req ->
CompletableFuture.completedFuture(new PlayerResponse(req.playerId(), "Mario", 1200)));
// request() returns a CompletionStage and requires a timeout
client.request("game-service", new GetPlayerRequest("p-100"), PlayerResponse.class, Duration.ofSeconds(3))
.thenAccept(player -> System.out.println("Player: " + player.name() + " (" + player.score() + " pts)"))
.exceptionally(err -> {
System.err.println("Request failed: " + err.getMessage());
return null;
});| Pattern | API | Semantics |
|---|---|---|
| Point-to-point request | request(target, req, type, timeout) |
Round-robin across the workers listening on the target |
| Broadcast query | broadcastRequest(target, req, type, timeout) |
Every worker on the target; first non-null response wins |
| Event | publish(channel, event) / subscribe(channel, type, handler) |
At-most-once broadcast to all subscribers |
| Module | What it does | Dependencies |
|---|---|---|
vyra-core |
Message bus, routing, correlation, timeouts, handler dispatch | none |
vyra-redis |
Redis transport (Lettuce): worker queues + Pub/Sub | Lettuce |
vyra-inmemory |
In-memory transport for tests and local dev | none |
vyra-jackson |
Jackson serializer: JSON, Smile, CBOR | Jackson |
vyra-gson |
Gson serializer: JSON | Gson |
Start here
- Getting started: the full tutorial, from zero to a working cluster
- Architecture: how Vyra works under the hood handling routing, correlation, timeouts and concurrency
- FAQ: quick answers to common questions
Core concepts
- Request / response: timeouts, broadcast, correlation
- Events: publish/subscribe semantics
- Transports: Redis setup, in-memory, writing a custom transport
- Serialization: JSON, Smile, CBOR and the uniform network constraint
Operations
- Configuration: node IDs, thread pools, builder options
- Concurrency: the threading model
- Timeouts & errors: the exception hierarchy
- Best practices: production checklist
Design
- ADRs: the recorded design decisions
Java 17 and Gradle 9.7.1 (wrapper included):
.\gradlew.bat build # Windows
./gradlew build # Linux/macOSModule-scoped tests: .\gradlew.bat :vyra-core:test and so on. The vyra-redis integration tests use Testcontainers
and need Docker.
Bug reports and feature requests go in the issue tracker. See CONTRIBUTING.md for the development workflow.
Apache License 2.0. See LICENSE.