Vert.x
Vert.x rules and best practices for Claude Code.
4.9k
5 TEMPLATES
Modern Vert.x Rules
# CLAUDE.md — Modern Vert.x ## Mental model - Vert.x is an **event-driven, non-blocking** toolkit. Code runs on a small pool of event-loop threads. - The **golden rule**: never block an event loop. If you need to block, use `executeBlocking` to run on a worker pool. - Code that violates the rule is the #1 source of Vert.x bugs. Profile early and often. ## Verticles - A **Verticle** is a unit of deployment. Like a fiber/actor with its own lifecycle. - Two flavors: - Standard: runs on the event loop. Default for HTTP servers, clients. - Worker: runs on a worker pool. Use for blocking work. - Deploy with `vertx.deployVerticle(MyVerticle.class.getName(), options)`. ## Async patterns Three styles in modern Vert.x — pick **one** per project: 1. **Callback style** — original API. Avoid in new code; nesting gets painful. 2. **Future-based** — `Future<T>` with `.compose`, `.map`, `.recover`. Cleaner than callbacks. 3. **Mutiny** — `Uni<T>` / `Multi<T>`. Reactive Streams compatible. The recommended style for new code, especially with Quarkus. ```java // Future style client.get("/users").send() .compose(resp -> validate(resp)) .map(body -> body.toJsonObject()) .onSuccess(json -> log.info(json)) .onFailure(err -> log.error("failed", err)); ``` ## Event loop discipline - Never call `Thread.sleep`, `Object.wait`, or any blocking I/O on an event loop thread. - Vert.x detects long-running event-loop callbacks and warns: `Thread Thread[vert.x-eventloop-thread-0] has been blocked for X ms`. - For genuinely blocking work, use: ```java vertx.executeBlocking(() -> doExpensiveThing(), false, ar -> { ... }); ``` - The `ordered=false` flag means Vert.x can run blocking calls in parallel. ## Configuration - `JsonObject` config passed to verticles via `DeploymentOptions.setConfig(...)`. - For env-driven config, use Vert.x Config (`io.vertx:vertx-config`) with sources: env, files, HTTP. - Parse to a typed POJO at boot — don't pass `JsonObject` through your code. ## Logging - Vert.x uses **java.util.logging** by default. Switch to SLF4J via system property: `-Dvertx.logger-delegate-factory-class-name=io.vertx.core.logging.SLF4JLogDelegateFactory`. - Always configure structured JSON logs in production. ## Threading model - Default: 2 × CPU event loops. Tune with `VertxOptions.setEventLoopPoolSize(...)`. - Worker pool: 20 threads default. For heavy blocking work, increase or split into custom worker executors. - Each verticle is pinned to one event loop. Don't share state across verticles via static fields — use the event bus. ## Don't - Don't `Thread.sleep`. Use `vertx.setTimer(ms, ...)`. - Don't hold mutable state across `.compose` boundaries. The continuation may run on a different thread. - Don't catch and ignore Future failures. Always handle them. - Don't block the event loop for any reason. If unsure, profile.Vert.x Web Routing
# CLAUDE.md — Vert.x Web Routing ## Setup - Add `io.vertx:vertx-web` for routing on top of Vert.x core. - Build a `Router`: ```java Router router = Router.router(vertx); router.get("/users/:id").handler(this::getUser); router.post("/users").handler(BodyHandler.create()).handler(this::createUser); vertx.createHttpServer().requestHandler(router).listen(8080); ``` - One router per HTTP server. For multi-tenancy or sub-app patterns, use sub-routers (`Router.router(vertx)`) and mount with `router.route("/api").subRouter(apiRouter)`. ## Handlers - Each handler takes a `RoutingContext`. Read params, body, headers; write response. - Always end the response: `ctx.json(dto)` or `ctx.response().end()`. Forgetting hangs the client. - Chain handlers with `.handler(...)`. Each can short-circuit by ending the response or throwing. ## Body parsing - `BodyHandler.create()` must come before any handler that reads the body. Mount it once at the top: ```java router.route().handler(BodyHandler.create()); ``` - For large bodies, set `setBodyLimit(maxBytes)`. - JSON: `ctx.body().asJsonObject()` or `ctx.body().asPojo(MyDto.class)` (with the right codec). ## Path & query params - `ctx.pathParam("id")` for path params. - `ctx.queryParam("page")` returns `List<String>` — multi-value aware. - For typed binding, use **OpenAPI Router Builder** (`OpenAPIContract.from(...)`) to generate routers from a spec with auto-validation. ## Validation - Use `vertx-web-validation` for declarative request validation: ```java ValidationHandler validation = ValidationHandlerBuilder.create(schemaParser) .pathParameter(Parameters.param("id", Schemas.stringSchema())) .body(Bodies.json(MY_SCHEMA)) .build(); router.post("/users").handler(validation).handler(this::createUser); ``` - Errors reach a final error handler — translate to JSON. ## Error handling - `router.errorHandler(500, ctx -> { ... })` for centralized error responses. - `ctx.fail(statusCode, throwable)` from inside a handler to escalate to the error handler. - Don't leak stack traces in production. Map to a clean error envelope. ## CORS - `CorsHandler.create("https://app.example.com").allowedMethod(...)`. - Mount before route handlers. - Don't use `*` origin with credentials. The browser will refuse. ## Auth - `AuthenticationHandler` (e.g., `JWTAuthHandler`) protects routes: ```java router.route("/admin/*").handler(JWTAuthHandler.create(jwtAuth)); ``` - Authorization (role checks) via `AuthorizationProvider` + `AuthorizationHandler`. ## Static files - `StaticHandler.create()` serves files from the classpath or filesystem. - For SPAs: route `/api/*` to handlers, fallback to `StaticHandler.create("dist").setIndexPage("index.html")`. ## Don't - Don't put the body handler after the route handler that reads the body — body will be empty. - Don't write blocking code in route handlers. Use `Future`, Mutiny `Uni`, or `executeBlocking`. - Don't write to the response after calling `end()`. Vert.x will throw or silently drop. - Don't use one giant `MainVerticle` with hundreds of routes. Split by feature into multiple verticles or routers.Vert.x with Mutiny (Reactive)
# CLAUDE.md — Vert.x with Mutiny ## Why Mutiny - **Mutiny** (`io.smallrye.mutiny:mutiny`) is a reactive library tightly integrated with Vert.x and Quarkus. - Cleaner than callback Vert.x. More fluent than Vert.x `Future`. Reactive Streams compatible (interop with Reactor, RxJava). - For new Vert.x code, prefer Mutiny or `Future`. Avoid the original callback API. ## Uni and Multi - `Uni<T>` — 0..1 element with possible failure. Most use cases. - `Multi<T>` — 0..N elements (a stream). For event streams, paginated reads, message subscriptions. ## Mutiny-flavored Vert.x - Use `io.vertx.mutiny.*` packages — every Vert.x API has a Mutiny version returning `Uni`/`Multi`: ```java io.vertx.mutiny.core.Vertx vertx = io.vertx.mutiny.core.Vertx.vertx(); io.vertx.mutiny.core.http.HttpClient client = vertx.createHttpClient(); Uni<String> body = client.request(GET, "https://example.com").flatMap(req -> req.send().flatMap(HttpClientResponse::body)).map(Buffer::toString); ``` - Don't import the non-Mutiny `io.vertx.core.*` types in Mutiny code. The signatures don't compose. ## Common operators - `.map(fn)` — synchronous transform. - `.onItem().transformToUni(fn)` — async transform returning a `Uni`. Equivalent to `flatMap`. - `.onFailure().recoverWithItem(default)` — fallback on error. - `.onFailure().retry().atMost(3)` — retry the upstream up to N times. - `.ifNoItem().after(Duration.ofSeconds(5)).fail()` — timeout. ## Combining Unis ```java Uni<Result> combined = Uni.combine().all().unis(uniA, uniB) .with(Result::new); ``` - `combine().all()` for "wait for all". `combine().any()` for "first that completes". - For collections: `Uni.combine().all().unis(listOfUnis).combinedWith(...)`. ## Subscribers - Mutiny is **lazy**. The pipeline doesn't run until subscribed. - In Vert.x Web with Mutiny: return `Uni<T>` from a handler — the framework subscribes for you. - Manual: `uni.subscribe().with(item -> ..., failure -> ...)`. ## Backpressure (Multi) - `Multi.createFrom().items(...)` for known finite streams. - `Multi.createFrom().publisher(reactivePublisher)` for streams from another reactive lib. - For backpressure: `.onOverflow().buffer(size)`, `.onOverflow().drop()`, etc. Choose based on what loss is acceptable. ## Error handling - `onFailure()` selectors: `.onFailure(IOException.class).recoverWithUni(fallback)`. - Always handle failures explicitly. Unhandled failures in `Uni` log a warning and disappear. - Don't catch in the underlying lambda — let Mutiny carry the failure. ## Concurrency - Mutiny respects the Vert.x event loop. Operators don't switch threads unless you ask. - `.runSubscriptionOn(executor)` to subscribe on a specific executor. - For blocking work inside a Uni chain: `.onItem().transformToUni(item -> Uni.createFrom().item(() -> blockingCall(item)).runSubscriptionOn(workerPool))`. ## Don't - Don't mix Mutiny with raw Vert.x callbacks in the same chain. Convert at the boundary. - Don't subscribe inside a handler — return the `Uni`. - Don't `await().atMost(...)` on Unis in production code. That's blocking, defeating the purpose. - Don't log inside `.map` lambdas — switch to `.invoke(item -> log.info(...))` for side-effect-only operators.Vert.x Event Bus
# CLAUDE.md — Vert.x Event Bus ## What it is - An in-process (and optionally cluster-wide) message bus baked into Vert.x. - Decouples senders from receivers — verticles communicate by **address** (a string), not by Java references. - Works in three modes: - **Point-to-point** — one consumer receives. - **Pub/Sub** — all consumers receive. - **Request/Reply** — sender gets a response. ## Sending messages ```java EventBus eb = vertx.eventBus(); // Point-to-point eb.send("orders.create", new JsonObject().put("userId", "123")); // Pub/Sub eb.publish("user.created", new JsonObject().put("id", "123")); // Request/Reply eb.request("orders.find", "order-123").onSuccess(reply -> { log.info("got order: {}", reply.body()); }); ``` - Addresses are simple strings. Use a hierarchical convention: `orders.create`, `orders.cancel`, `users.update`. - Don't put PII or large payloads in event-bus messages — they're broadcast across the cluster. ## Consuming ```java eb.consumer("orders.create", message -> { JsonObject body = (JsonObject) message.body(); OrderId id = createOrder(body); message.reply(new JsonObject().put("id", id.toString())); }); ``` - Always reply on a request — if you don't, the sender gets a timeout. - Use `message.fail(code, msg)` to signal an error to the sender. - Unsubscribe via the `MessageConsumer` returned by `consumer(...)` when shutting down. ## Codecs - Default codecs: `String`, `JsonObject`, `JsonArray`, `Buffer`, primitive boxed types. - For custom POJOs, register a codec: ```java eb.registerDefaultCodec(MyDto.class, new MyDtoCodec()); ``` - For cluster mode, all nodes must register the same codecs. Otherwise messages won't deserialize. ## Clustering - Vert.x can cluster across nodes via Hazelcast, Infinispan, Ignite, or ZooKeeper. - The same `EventBus` API works locally and in cluster mode. - For cluster mode: `Vertx.clusteredVertx(options, ar -> ...)`. - Network split-brain handling: configure the underlying clustering layer carefully. Defaults are not production-ready. ## Patterns - **Service-locator alternative**: register handlers as services on the bus instead of injecting them. - **Saga orchestration**: each step publishes an event; coordinator listens and dispatches the next step. - **Cache invalidation**: publish on `cache.invalidate.{key}`; every node consumes and updates its local cache. ## Performance - In-process: ~1–10 µs per message. - Clustered: depends on the cluster manager. Hazelcast adds ~1 ms per hop. - For high throughput, batch messages or use direct verticle calls instead of the event bus. ## Service proxies - Vert.x can generate type-safe proxies for an interface — calls become event-bus messages under the hood: ```java @ProxyGen public interface UserService { void findById(String id, Handler<AsyncResult<JsonObject>> handler); } ``` - Generated code (annotation processor) creates the proxy and the codec. - Useful for cross-language clients in clustered setups. ## Don't - Don't use the event bus for high-throughput hot paths between two verticles in the same JVM. Direct method calls are faster. - Don't broadcast sensitive data on a clustered bus without encryption (`SSLContext` on the cluster manager). - Don't forget to reply on `request` consumers. Senders will time out. - Don't use mutable shared objects as message bodies. Use immutable JSON or POJOs with codecs.Vert.x + Reactive Postgres
# CLAUDE.md — Vert.x + Reactive Postgres ## Stack - **vertx-pg-client** — fully reactive, non-blocking Postgres client. No JDBC. - For Mutiny-flavored API: `io.vertx.mutiny.pgclient.PgPool`. - Pair with **Flyway** or **Liquibase** for schema migrations (those are blocking; run as a separate startup step). ## Pool setup ```java PgConnectOptions connect = new PgConnectOptions() .setHost("localhost").setPort(5432) .setDatabase("app").setUser("app").setPassword(secret); PoolOptions pool = new PoolOptions().setMaxSize(10); Pool client = PgPool.pool(vertx, connect, pool); ``` - Pool size: start at 10. Tune based on `EXPLAIN`-driven query duration and concurrent users. - Reuse the `Pool` for the app's lifetime. Don't create per-request. ## Queries ```java client.query("SELECT id, email FROM users").execute() .onSuccess(rows -> rows.forEach(row -> log.info(row.getString("email")))) .onFailure(Throwable::printStackTrace); ``` - `query(...)` for simple queries. `preparedQuery(...)` for parameterized — use it always except for static SQL. - Parameterized: ```java client.preparedQuery("SELECT id FROM users WHERE email = $1") .execute(Tuple.of(email)) .onSuccess(rs -> ...); ``` - Postgres uses `$1`, `$2`, ... — not `?`. ## Mapping rows - For typed access, use the row mapper: ```java RowMapper<User> mapper = row -> new User(row.getUUID("id"), row.getString("email")); ``` - Or define a function and `.map(mapper)` over the results. - Don't return raw `Row` to callers. Wrap into a domain type. ## Transactions ```java client.withTransaction(conn -> { return conn.query("INSERT INTO orders ...").execute() .compose(__ -> conn.query("UPDATE inventory ...").execute()); }).onComplete(ar -> ...); ``` - `withTransaction` commits on success and rolls back on failure automatically. - Use it for any multi-statement write. - Read-only transactions: skip — postgres handles them efficiently without explicit BEGIN. ## Prepared statements - The pool caches prepared statements per connection. Reuse query strings for free benefit. - Avoid string-concatenating SQL — that defeats the cache and exposes you to injection. - For dynamic IN clauses, use `ARRAY[$1::uuid[]]` patterns rather than building lists. ## Migrations - Use Flyway for migrations. It's JDBC-based, runs at startup, blocking — that's fine because it's a one-time bootstrap step. - After migrations succeed, the app boots Vert.x and uses the reactive client. - Don't `executeBlocking(flywayMigrate)` from inside a verticle. ## Bulk operations - For large inserts: `executeBatch(batchedTuples)` — single round-trip. - For COPY: use the `pgPool.connection()` API to access raw COPY support if needed. ## Connection limits - Postgres default is 100 connections. With pgbouncer, can be much higher. - Set `setIdleTimeout` to release idle connections eventually. - Don't size the pool larger than `(server max conns / replicas)` minus overhead. ## Don't - Don't use JDBC drivers in Vert.x apps. They block the event loop. - Don't share a connection across coroutines/handlers. Use the pool — it manages connection lifecycle. - Don't put migrations in the same `Pool` lifecycle as queries — JDBC vs reactive are different APIs. - Don't ignore the `onFailure` callback. Postgres errors silently disappearing is a common bug.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.