{"id":51468692,"url":"https://github.com/damianmalczewski/natspring","last_synced_at":"2026-07-06T14:30:43.134Z","repository":{"id":359872258,"uuid":"1247814945","full_name":"damianmalczewski/natspring","owner":"damianmalczewski","description":"Annotation-based listeners for NATS in Spring Boot","archived":false,"fork":false,"pushed_at":"2026-07-02T05:40:05.000Z","size":624,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-02T07:22:46.407Z","etag":null,"topics":["annotation","autoconfiguration","library","listener","nats","spring-boot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/damianmalczewski.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-23T20:21:36.000Z","updated_at":"2026-07-02T05:40:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/damianmalczewski/natspring","commit_stats":null,"previous_names":["damianmalczewski/natsify","damianmalczewski/natspring"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/damianmalczewski/natspring","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damianmalczewski%2Fnatspring","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damianmalczewski%2Fnatspring/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damianmalczewski%2Fnatspring/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damianmalczewski%2Fnatspring/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/damianmalczewski","download_url":"https://codeload.github.com/damianmalczewski/natspring/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damianmalczewski%2Fnatspring/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35195590,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["annotation","autoconfiguration","library","listener","nats","spring-boot"],"created_at":"2026-07-06T14:30:42.323Z","updated_at":"2026-07-06T14:30:43.111Z","avatar_url":"https://github.com/damianmalczewski.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Natspring Project\n\n[![Build Status](https://github.com/damianmalczewski/natspring/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/damianmalczewski/natspring/actions/workflows/gradle-build.yml)\n[![Xodecov](https://codecov.io/gh/damianmalczewski/natspring/graph/badge.svg?token=6NQ031426J)](https://codecov.io/gh/damianmalczewski/natspring)\n[![Sonatype](https://img.shields.io/maven-central/v/io.github.malczuuu.natspring/natspring-starter)][maven-central]\n[![License](https://img.shields.io/badge/license-Apache%202.0-green)](https://github.com/damianmalczewski/natspring/blob/main/LICENSE)\n\nSpring Boot auto-configuration for [NATS](https://nats.io), providing annotation-driven message listeners for both core\nNATS and JetStream.\n\nRequires Spring Boot 4.x.\n\n\u003e [!IMPORTANT]\n\u003e This project began as an educational exercise for learning how to create `@KafkaListener`-alike, annotation-based\n\u003e message listeners. It has no roadmap and its evolution will most likely be driven by personal interest and/or\n\u003e contributions.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Configuration](#configuration)\n    - [Options customizer](#options-customizer)\n    - [Connection hooks](#connection-hooks)\n- [Listener annotations](#listener-annotations)\n    - [`@NatsListener`](#natslistener)\n    - [`@JetStreamListener`](#jetstreamlistener)\n- [Dead-lettering](#dead-lettering)\n    - [Core NATS](#core-nats)\n    - [JetStream](#jetstream)\n    - [Dead-letter headers](#dead-letter-headers)\n- [Method parameter types](#method-parameter-types)\n    - [Parameter annotations](#parameter-annotations)\n        - [`@NatsPayload`](#natspayload)\n        - [`@NatsHeader`](#natsheader)\n        - [`@NatsHeaders`](#natsheaders)\n        - [`@NatsSubject`](#natssubject)\n        - [`@NatsReplyTo`](#natsreplyto)\n    - [JSON deserialization](#json-deserialization)\n    - [No-arg methods](#no-arg-methods)\n    - [Mixed parameters](#mixed-parameters)\n    - [Message interceptors](#message-interceptors)\n- [Request-reply (RPC)](#request-reply-rpc)\n    - [Listener return values](#listener-return-values)\n    - [Sending requests](#sending-requests)\n- [JetStream stream auto-creation](#jetstream-stream-auto-creation)\n- [NatsOperations](#natsoperations)\n    - [Publishing](#publishing)\n    - [Headers](#headers)\n    - [Publish interceptors](#publish-interceptors)\n- [Observability](#observability)\n    - [Health](#health)\n    - [Metrics](#metrics)\n- [Testing](#testing)\n    - [Testcontainers](#testcontainers)\n- [Building from source](#building-from-source)\n- [License](#license)\n\n## Installation\n\n[![Sonatype](https://img.shields.io/maven-central/v/io.github.malczuuu.natspring/natspring-starter)][maven-central]\n\nAdd the starter module to your dependencies.\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.malczuuu.natspring\u003c/groupId\u003e\n    \u003cartifactId\u003enatspring-starter\u003c/artifactId\u003e\n    \u003cversion\u003e{version}\u003c/version\u003e\n\u003c/dependency\u003e\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.malczuuu.natspring\u003c/groupId\u003e\n    \u003cartifactId\u003enatspring-starter-test\u003c/artifactId\u003e\n    \u003cversion\u003e{version}\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n```\n\n```kotlin\ndependencies {\n    implementation(\"io.github.malczuuu.natspring:natspring-starter:{version}\")\n    testImplementation(\"io.github.malczuuu.natspring:natspring-starter-test:{version}\")\n}\n```\n\n## Configuration\n\n- `nats.enabled` - Whether NATS auto-configuration is enabled. Default: `true`.\n- `nats.server` - NATS server URL. Maps to `server(String)`. Default: `nats://localhost:4222`.\n- `nats.username` - Username for NATS authentication. Combined with `password` into `userInfo(String, char[])`. Default:\n  _(none)_.\n- `nats.password` - Password for NATS authentication. Combined with `username` into `userInfo(String, char[])`. Default:\n  _(none)_.\n- `nats.auto-stream-creation` - Whether declared `StreamConfiguration` beans are used to create streams on startup.\n  Default: `false`.\n- `nats.pull-fetch-batch-size` - Number of messages fetched per poll cycle for JetStream pull consumers. Default: `200`.\n- `nats.pull-fetch-timeout` - Maximum time to wait for messages in each fetch call for JetStream pull consumers.\n  Default: `200ms`.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eAdditional properties (see more...)\u003c/b\u003e\u003c/summary\u003e\n\nAll properties in this section are nullable. Setting a property to `null` skips the related `io.nats.client.Options`\ncall and the NATS Java client's built-in default applies.\n\n- `nats.connection-name` - Name for the NATS connection; falls back to `spring.application.name`. Maps to\n  `connectionName(String)`. Default: `null`.\n- `nats.connection-timeout` - Maximum time to wait when establishing a connection. Maps to\n  `connectionTimeout(Duration)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.socket-write-timeout` - Maximum time to wait for a socket write to complete. Maps to\n  `socketWriteTimeout(Duration)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.max-reconnects` - Maximum reconnect attempts before giving up; `-1` means unlimited. Maps to\n  `maxReconnects(int)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.reconnect-wait` - Time to wait between reconnect attempts. Maps to `reconnectWait(Duration)`. Default: `null`\n  (`2s` per NATS Java client).\n- `nats.reconnect-jitter` - Random jitter added to `reconnect-wait` for non-TLS connections. Maps to\n  `reconnectJitter(Duration)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.reconnect-jitter-tls` - Random jitter added to `reconnect-wait` for TLS connections. Maps to\n  `reconnectJitterTls(Duration)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.reconnect-buffer-size` - Size in bytes of the buffer used to hold messages while reconnecting. Maps to\n  `reconnectBufferSize(long)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.ping-interval` - Interval between client-side pings to the server. Maps to `pingInterval(Duration)`. Default:\n  `null` (so the value of native NATS client is not overwritten).\n- `nats.max-pings-out` - Maximum outstanding pings without a response before the connection is considered stale. Maps to\n  `maxPingsOut(int)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.request-cleanup-interval` - Interval at which the client scans for timed-out pending requests. Maps to\n  `requestCleanupInterval(Duration)`. Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.inbox-prefix` - Prefix for auto-generated inbox subjects (must end with `.`). Maps to `inboxPrefix(String)`.\n  Default: `null` (so the value of native NATS client is not overwritten).\n- `nats.no-echo` - Suppress echoing published messages back to the sending connection. Maps to `noEcho()`. Default:\n  `false`.\n- `nats.no-randomize` - Disable randomization of the server list on connect and reconnect. Maps to `noRandomize()`.\n  Default: `false`.\n\n\u003c/details\u003e\n\n### Options customizer\n\nFor connection options not covered by `nats.*` properties, declare a `ConnectionOptionsBuilderCustomizer`bean. It\nreceives an `io.nats.client.Options.Builder` before the connection is established and can apply any configuration the\nnative NATS Java client supports.\n\n```java\n@Configuration\npublic class NatsCustomizerConfiguration {\n\n  @Bean\n  public ConnectionOptionsBuilderCustomizer tlsCustomizer() {\n    return builder -\u003e builder.sslContext(buildSslContext());\n  }\n}\n```\n\nMultiple customizer beans are applied in the order determined by `org.springframework.core.Ordered`. Customizers run\nafter the built-in property-based configuration, so they take precedence over `nats.*` properties for the same option.\n\n### Connection hooks\n\n\u003e [!IMPORTANT]\n\u003e This feature is experimental and most likely will change after more use cases are discovered.\n\nThe native `io.nats.client.Connection` cannot be injected as a Spring bean. Current library manages it internally via\nSpring's `SmartLifecycle` and controls when it is opened and closed. For example, when `@SpringBootTest`-annotated tests\nswitch context due to different Spring configurations, the subscriptions are stopped, the connection is closed, and the\nfresh one for new context is created and started. When we go back to the previous context, the connection is recreated\nagain and so on. One connection never interferes with another.\n\nAny logic that depends directly on the native connection - such as creating subscriptions without annotations provided\nby current library, dispatchers, or key-value store handles - must be performed through a `ConnectionHook` bean. This\nensures that such resources are properly set up after each connect and torn down before each close.\n\n```java\n@Component\npublic class CustomSubscriptionHook implements ConnectionHook {\n\n  @Override\n  public void postConnect(Connection connection) {\n    connection.createDispatcher().subscribe(\"custom.subject\", msg -\u003e {\n      // handle message\n    });\n  }\n  \n  // you can also override preClose(Connection connection)\n}\n```\n\nMultiple `ConnectionHook` beans are invoked in the order determined by `org.springframework.core.Ordered`.\n\n## Listener annotations\n\n### `@NatsListener`\n\nSubscribes to a core NATS subject. Supports queue groups for load balancing.\n\n```java\n@NatsListener(subject = \"orders.placed\")\npublic void onOrder(Order order) {}\n\n@NatsListener(subject = \"orders.placed\", queue = \"order-processors\")\npublic void onOrderQueued(Order order) {}\n```\n\n| Attribute           | Description                                                                                  |\n|---------------------|----------------------------------------------------------------------------------------------|\n| `subject`           | NATS subject pattern (wildcards `*` and `\u003e` supported). Supports `${property}` placeholders. |\n| `queue`             | Optional queue group name for competing-consumer load balancing.                             |\n| `deadLetterSubject` | Optional subject to publish failed messages to. Empty string (default) disables DLQ.         |\n\n### `@JetStreamListener`\n\nSubscribes to a JetStream subject using a durable push consumer with explicit ack. On successful return the message is\nacked; on handler exception it is nacked; on argument resolution failure the action is controlled by `onResolveFailure`\n(default: `TERM`).\n\n```java\n@JetStreamListener(subject = \"orders.\u003e\", stream = \"ORDERS\", durable = \"order-processor\")\npublic void onOrder(Order order) {}\n\n@JetStreamListener(\n    subject = \"orders.\u003e\",\n    stream = \"ORDERS\",\n    durable = \"order-proc-grp\",\n    queue = \"processors\")\npublic void onOrderQueued(Order order) {}\n```\n\n| Attribute              | Description                                                                                |\n|------------------------|--------------------------------------------------------------------------------------------|\n| `subject`              | Subject pattern to filter within the stream. Supports `${property}` placeholders.          |\n| `stream`               | JetStream stream name. Optional; NATS will infer from the subject if omitted.              |\n| `durable`              | Durable consumer name. Omit for an ephemeral consumer.                                     |\n| `queue`                | Optional queue group name for competing-consumer load balancing.                           |\n| `deadLetterSubject`    | Optional subject to publish failed messages to. Empty string (default) disables DLQ.       |\n| `deadLetterDeliveries` | Maximum delivery attempts before dead-lettering. Required when `deadLetterSubject` is set. |\n| `ackMode`              | `AUTO` (default) acks on success and nacks on failure; `MANUAL` leaves ack to the handler. |\n| `deliverPolicy`        | Which messages to receive on first connect: `NEW` (default), `ALL`, or `LAST`.             |\n| `consumerType`         | `PULL` (default) or `PUSH`.                                                                |\n| `onResolveFailure`     | Action on argument resolution failure: `TERM` (default), `NAK`, or `DISCARD`.              |\n\n## Dead-lettering\n\nBoth listener types support a `deadLetterSubject` attribute. When set, failed messages are published to that subject\ninstead of being silently dropped. All original message headers are forwarded, and additional `X-Dead-Letter-*` headers\nare added (see [Dead-letter headers](#dead-letter-headers) below).\n\n### Core NATS\n\nCore NATS has no persistence or redelivery. Dead-lettering is **at-most-once**: a failure publishes to the DLQ\nimmediately and the original message is gone regardless.\n\n```java\n@NatsListener(subject = \"orders.placed\", deadLetterSubject = \"orders.placed.dlq\")\npublic void onOrder(Order order) { /* ... */ }\n```\n\nBoth argument resolution failures (malformed payload) and handler invocation failures dead-letter on the first attempt.\nIf the DLQ publish itself fails, the error is logged and the message is dropped.\n\n### JetStream\n\nJetStream has persistence and delivery tracking, so dead-lettering integrates with the retry lifecycle:\n\n```java\n@JetStreamListener(\n    subject = \"orders.\u003e\",\n    stream = \"ORDERS\",\n    durable = \"order-processor\",\n    deadLetterSubject = \"orders.dlq\",\n    maxDeliveries = 3)\npublic void onOrder(Order order) { /* ... */ }\n```\n\n| Failure type                | Behaviour                                                                                                                                                                                          |\n|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Argument resolution failure | Controlled by `onResolveFailure`. Default (`TERM`): published to DLQ immediately, then `term()`-ed. `NAK`: nacked and redelivered; dead-lettered and `term()`-ed once `deadLetterDeliveries` is exhausted. `DISCARD`: silently `ack()`-ed, no DLQ. |\n| Handler invocation failure  | Message is `nak()`-ed and redelivered up to `deadLetterDeliveries` times, then published to DLQ and `term()`-ed.                                                                                   |\n\nIf the DLQ publish itself fails, the exception propagates: the message is **not** terminated and will be redelivered.\nThis may push the delivery count above `maxDeliveries`, which is intentional - the message is retried until the DLQ\nbecomes reachable rather than being lost.\n\n### Dead-letter headers\n\nEvery dead-letter message carries the following headers in addition to all headers from the original message.\n\n- `X-Dead-Letter-Subject` - original subject the message was received on                                    \n- `X-Dead-Letter-Reason` - exception simple name and message, truncated to 200 characters                  \n- `X-Dead-Letter-Exception` - fully-qualified exception class name                                            \n- `X-Dead-Letter-Timestamp` - ISO-8601 UTC timestamp of the dead-letter publish                               \n- `X-Dead-Letter-Stream` - JetStream stream name, not available from `@NatsListener`                       \n- `X-Dead-Letter-Durable` - durable consumer name, not available from `@NatsListener`                       \n- `X-Dead-Letter-Delivery` - delivery count at the time of dead-lettering, not available from `@NatsListener`\n\n## Method parameter types\n\nParameters are resolved in the order listed below. The first match wins.\n\n| Priority | Condition                                                                | Resolved value                                             |\n|----------|--------------------------------------------------------------------------|------------------------------------------------------------|\n| 1        | Parameter type is `io.nats.client.Message` (or subtype)                  | Raw NATS message                                           |\n| 2        | Parameter annotated with `@NatsHeader`                                   | Header value(s) as `String`, `List\u003cString\u003e`, or `String[]` |\n| 3        | Parameter annotated with `@NatsSubject`                                  | Message subject as `String`                                |\n| 4        | Parameter annotated with `@NatsHeaders`                                  | All headers as `io.nats.client.impl.Headers`               |\n| 5        | Parameter type is `io.nats.client.impl.Headers` (without `@NatsPayload`) | All headers as `io.nats.client.impl.Headers`               |\n| 6        | Parameter type is `NatsJetStreamMetaData` (without `@NatsPayload`)       | JetStream message metadata                                 |\n| 7        | Parameter type is `byte[]`                                               | Raw message body bytes                                     |\n| 8        | Parameter type is `String`                                               | Message body decoded as UTF-8                              |\n| 9        | Any other type, or `@NatsPayload`-annotated parameter                    | Message body deserialized from JSON                        |\n\n### Parameter annotations\n\n### `@NatsPayload`\n\nMarks a parameter explicitly as the message payload. If the type is easily distinguishable (`byte[]`, `String` or a POJO\nclass/record), the annotation can be omitted and the parameter will be resolved as payload by default. Recommended\nkeeping for clarity and/or documentation purposes.\n\n```java\n@NatsListener(subject = \"raw.events\")\npublic void handle(@NatsPayload byte[] body) { /* ... */ }\n\n@NatsListener(subject = \"text.events\")\npublic void handle(@NatsPayload String text) { /* ... */ }\n\n@NatsListener(subject = \"json.events\")\npublic void handle(@NatsPayload List\u003cEvent\u003e events) { /* ... */ }\n```\n\n### `@NatsHeader`\n\nInjects a header value by name. Resolved as `String` (first value), `List\u003cString\u003e`, or `String[]` (all values) depending\non the parameter type.\n\n```java\n@NatsListener(subject = \"events\")\npublic void handle(Event event, @NatsHeader(\"X-Correlation-Id\") String correlationId) { /* ... */ }\n\n@NatsListener(subject = \"events\")\npublic void handle(@NatsHeader(\"X-Tags\") List\u003cString\u003e tags) { /* ... */ }\n\n@NatsListener(subject = \"events\")\npublic void handle(@NatsHeader(\"X-Tags\") String[] tags) { /* ... */ }\n```\n\n### `@NatsHeaders`\n\nInjects all message headers. Equivalent to declaring `io.nats.client.impl.Headers` as the parameter type, but explicit.\n\n```java\n@NatsListener(subject = \"events\")\npublic void handle(Event event, @NatsHeaders Headers headers) { /* ... */ }\n```\n\n### `@NatsSubject`\n\nInjects the subject the message was published to. Useful when a listener matches a wildcard subject\nand needs to inspect the concrete subject at runtime.\n\n```java\n@NatsListener(subject = \"events.\u003e\")\npublic void handle(Event event, @NatsSubject String subject) {}\n\n@JetStreamListener(subject = \"orders.\u003e\", stream = \"ORDERS\", durable = \"router\")\npublic void handle(Order order, @NatsSubject String subject) { /* ... */ }\n```\n\n### `@NatsReplyTo`\n\nInjects the reply-to inbox from the incoming message as a `String`. The value is `null` when the message has no reply-to\naddress (i.e., was published without a reply inbox). Only supported on `@NatsListener` methods.\n\n```java\n@NatsListener(subject = \"events\")\npublic void handle(Event event, @NatsReplyTo String replyTo) {\n    if (replyTo != null) {\n        natsOperations.publish(replyTo, new EventAck(event.id()));\n    }\n}\n```\n\n### JSON deserialization\n\nAny parameter not matched by the rules above is deserialized from the message body using Jackson. Full generic type\ninformation is preserved, so `List\u003cOrder\u003e`, `Order[]`, and other parameterized types work correctly.\n\n```java\n@NatsListener(subject = \"batch.orders\")\npublic void onBatch(List\u003cOrder\u003e orders) { /* ... */ }\n\n@NatsListener(subject = \"batch.orders\")\npublic void onBatch(Order[] orders) { /* ... */ }\n```\n\n### No-arg methods\n\nMethods with no parameters are supported. The message is received and discarded.\n\n```java\n@NatsListener(subject = \"ping\")\npublic void onPing() { /* ... */ }\n```\n\n### Mixed parameters\n\nA method may declare any combination of the above in any order.\n\n```java\n@JetStreamListener(subject = \"orders.\u003e\", stream = \"ORDERS\", durable = \"auditor\")\npublic void onOrder(\n    Order order, @NatsHeader(\"X-Source\") String source, Headers allHeaders, Message rawMessage) {\n  // ...\n}\n```\n\n### Message interceptors\n\n`NatsMessageInterceptor` beans are discovered automatically and applied to every inbound message in order, for both\n`@NatsListener` and `@JetStreamListener` methods. The interceptor receives the raw `io.nats.client.Message` before any\nargument resolution or deserialization.\n\n```java\n@Component\npublic class TracingMessageInterceptor implements NatsMessageInterceptor {\n\n  private final Tracer tracer;\n\n  public TracingMessageInterceptor(Tracer tracer) {\n    this.tracer = tracer;\n  }\n\n  @Override\n  public void intercept(Message message, NatsMessageInterceptorChain chain) {\n    String traceId =\n        message.getHeaders() != null ? message.getHeaders().getFirst(\"X-Trace-Id\") : null;\n    tracer.setCurrentTraceId(traceId != null ? traceId : tracer.newTraceId());\n    try {\n      chain.proceed(message);\n    } finally {\n      tracer.clearCurrentTraceId();\n    }\n  }\n}\n```\n\nNot calling `chain.proceed()` drops the message silently. For JetStream listeners, a dropped message will be\nredelivered by the broker unless the interceptor explicitly acknowledges or terminates it beforehand.\n\nMultiple interceptors are sorted by their `getOrder()` value (`Ordered.LOWEST_PRECEDENCE` by default).\n\n## Request-reply (RPC)\n\nNATS supports a built-in request-reply (RPC) pattern. The requester sends a message with a reply-to inbox address; the\nresponder publishes the reply to that address.\n\n### Listener return values\n\nA `@NatsListener` method can participate as a responder by returning a non-void value. When the incoming message\ncarries a reply-to address, the return value is automatically serialized and published to that address.\n\n| Return type              | Reply body                                 |\n|--------------------------|--------------------------------------------|\n| `byte[]`                 | Sent as-is                                 |\n| `String`                 | UTF-8 encoded bytes                        |\n| `io.nats.client.Message` | Body and headers from the returned message |\n| Any other type           | JSON-serialized via Jackson                |\n\n```java\n@NatsListener(subject = \"calc.add\")\npublic MathResult handleAdd(MathRequest request) {\n    return new MathResult(request.a() + request.b());\n}\n\n@NatsListener(subject = \"ping\")\npublic String handlePing() {\n    return \"pong\";\n}\n```\n\nIf the incoming message has no reply-to address and the method returns a non-null value, the reply is discarded and a\n`WARN` is logged. This allows a subject to be used for both fire-and-forget and RPC without reconfiguring the listener.\n\nReturn values are not supported on `@JetStreamListener` methods. JetStream uses the reply-to field internally for\nacknowledgment.\n\n### Sending requests\n\n`NatsOperations` provides `request()` methods that send a message and return a `CompletableFuture\u003cNatsReply\u003e` that\ncompletes when a reply arrives. The future completes exceptionally with `TimeoutException` if no reply arrives within\nthe given timeout.\n\n```java\nclass RequestExample {\n    \n  private final NatsOperations natsOperations;\n\n  public void requestExamples() {\n    // bytes\n    NatsReply reply = natsOperations.request(\"ping\", new byte[0], Duration.ofSeconds(5)).get();\n\n    // string\n    NatsReply reply = natsOperations.request(\"ping\", \"data\", Duration.ofSeconds(5)).get();\n\n    // JSON-serialized object - reply deserialized with bodyAs()\n    NatsReply reply =\n        natsOperations.request(\"calc.add\", new MathRequest(3, 4), Duration.ofSeconds(5)).get();\n    MathResult result = reply.bodyAs(MathResult.class);\n  }\n}\n```\n\n`NatsReply` wraps the raw `io.nats.client.Message` and adds `bodyAs(Class\u003cT\u003e)` / `bodyAs(TypeReference\u003cT\u003e)` for JSON\ndeserialization without a separate `ObjectMapper`.\n\n## JetStream stream auto-creation\n\nDeclare `io.nats.client.api.StreamConfiguration` beans and the auto-configuration will create the corresponding streams \non startup (if they do not already exist), before any listeners are registered.\n\n\u003e [!IMPORTANT]\n\u003e Works only if `nats.auto-stream-creation` is set to `true` (disabled by default).\n\n```java\n@Configuration\npublic class ExampleConfiguration {\n\n  @Bean\n  public StreamConfiguration ordersStream() {\n    return StreamConfiguration.builder().name(\"ORDERS\").subjects(\"orders.\u003e\").build();\n  }\n}\n```\n\n## NatsOperations\n\n`NatsOperations` is the primary API for publishing messages and sending requests. It is auto-configured as a Spring bean\nand available for injection.\n\n```java\n@Service\npublic class OrderService {\n\n  private final NatsOperations natsOperations;\n\n  public OrderService(NatsOperations natsOperations) {\n    this.natsOperations = natsOperations;\n  }\n}\n```\n\n### Publishing\n\nThree payload types are supported out of the box:\n\n```java\nclass OrderService {\n\n  private final NatsOperations natsOperations;\n\n  void publishExamples() {\n    natsOperations.publish(\"orders.placed\", rawBytes); // byte[]\n    natsOperations.publish(\"orders.placed\", \"plain text\"); // String - UTF-8 encoded\n    natsOperations.publish(\"orders.placed\", new Order(\"id\", \"...\")); // JSON via Jackson\n  }\n}\n```\n\nA pre-built `io.nats.client.Message` can be published as-is for full control:\n\n```java\nclass OrderService {\n\n  private final NatsOperations natsOperations;\n\n  void publishMessage() {\n    Message message = NatsMessage.builder().subject(\"orders.placed\").data(rawBytes).build();\n    natsOperations.publish(message);\n  }\n}\n```\n\n### Headers\n\nEvery publish variant has an overload that accepts `io.nats.client.impl.Headers`:\n\n```java\nclass OrderService {\n\n  private final NatsOperations natsOperations;\n  private final AppInfo appInfo;\n\n  void publishWithHeaders() {\n    Headers headers = new Headers();\n    headers.add(\"X-Publisher-App\", appInfo.getAppName());\n\n    natsOperations.publish(\"orders.placed\", headers, rawBytes); // byte[]\n    natsOperations.publish(\"orders.placed\", headers, \"plain text\"); // String - UTF-8 encoded\n    natsOperations.publish(\"orders.placed\", headers, new Order(\"id\", \"...\")); // JSON via Jackson\n  }\n}\n```\n\n`request()` does not have a headers overload - attach headers via a [publish interceptor](#publish-interceptors) or\nbuild a `Message` and use `publish()` manually.\n\n### Publish interceptors\n\n`NatsPublishInterceptor` beans are discovered automatically and applied to every outbound message in order, including\nrequests. Interceptors receive a fully-built `Message` (subject, headers, serialized body) regardless of which\n`publish()` or `request()` overload was called.\n\n```java\n@Component\npublic class TracingPublishInterceptor implements NatsPublishInterceptor {\n\n  private final Tracer tracer;\n\n  public TracingPublishInterceptor(Tracer tracer) {\n    this.tracer = tracer;\n  }\n\n  @Override\n  public void intercept(Message message, NatsPublishInterceptorChain chain) {\n    Headers enriched = message.getHeaders() != null ? message.getHeaders() : new Headers();\n    enriched.add(\"X-Trace-Id\", tracer.currentTraceId());\n    chain.proceed(\n        NatsMessage.builder()\n            .subject(message.getSubject())\n            .headers(enriched)\n            .data(message.getData())\n            .build());\n  }\n}\n```\n\nNot calling `chain.proceed()` suppresses the message entirely. For requests, a suppressed message causes the returned\n`CompletableFuture` to complete exceptionally with `IllegalStateException`.\n\nMultiple interceptors are sorted by their `getOrder()` value (`Ordered.LOWEST_PRECEDENCE` by default).\n\n## Observability\n\n### Health\n\nWhen `spring-boot-actuator` is on the classpath, a `NatsHealthIndicator` is auto-configured under the `nats` component\nname. It reports `UP` when the connection status is `CONNECTED` and `DOWN` for any other status, including connection\nloss or failure to obtain the connection.\n\n```json\n{\n  \"components\": {\n    \"nats\": {\n      \"status\": \"UP\",\n      \"details\": { \"connectionStatus\": \"CONNECTED\" }\n    }\n  }\n}\n```\n\n### Metrics\n\nWhen Micrometer is on the classpath, the following meters are auto-configured. All observer beans can be replaced by\ndeclaring a custom implementation in the application context.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eCore NATS listener metrics (see more...)\u003c/b\u003e\u003c/summary\u003e\n\nTagged with `subject` and `queue`.\n\n| Meter                                 | Type    | Description                                 |\n|---------------------------------------|---------|---------------------------------------------|\n| `nats.listener.messages.received`     | Counter | Messages received before handler invocation |\n| `nats.listener.messages.success`      | Counter | Messages handled without exception          |\n| `nats.listener.messages.error`        | Counter | Messages that caused a handler exception    |\n| `nats.listener.messages.deadlettered` | Counter | Messages published to a dead-letter subject |\n| `nats.listener.messages.duration`     | Timer   | Handler processing time                     |\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eJetStream listener metrics (see more...)\u003c/b\u003e\u003c/summary\u003e\n\nTagged with `subject` and `stream`.\n\n| Meter                                  | Type    | Description                                                                       |\n|----------------------------------------|---------|-----------------------------------------------------------------------------------|\n| `nats.jetstream.messages.received`     | Counter | Messages received before handler invocation                                       |\n| `nats.jetstream.messages.acked`        | Counter | Messages acked after successful handling                                          |\n| `nats.jetstream.messages.nacked`       | Counter | Messages nacked after a handler exception                                         |\n| `nats.jetstream.messages.terminated`   | Counter | Messages terminated (e.g. deserialization failure). Also tagged with `exception`. |\n| `nats.jetstream.messages.deadlettered` | Counter | Messages dead-lettered after exhausting delivery attempts                         |\n| `nats.jetstream.messages.duration`     | Timer   | Handler processing time                                                           |\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eConnection metrics (see more...)\u003c/b\u003e\u003c/summary\u003e\n\n| Meter                                        | Type    | Tags        | Description                                      |\n|----------------------------------------------|---------|-------------|--------------------------------------------------|\n| `nats.connection.events`                     | Counter | `nats`      | Connection state-change events                   |\n| `nats.connection.errors`                     | Counter | `error`     | Server error strings received                    |\n| `nats.connection.exceptions`                 | Counter | `exception` | Client-side exceptions during processing         |\n| `nats.connection.slow.consumer.detected`     | Counter | -           | Slow consumer detections                         |\n| `nats.connection.message.discarded`          | Counter | -           | Messages discarded due to a full consumer queue  |\n| `nats.statistics.pings`                      | Gauge   | -           | Total pings sent                                 |\n| `nats.statistics.reconnects`                 | Gauge   | -           | Total reconnect attempts                         |\n| `nats.statistics.in.msgs`                    | Gauge   | -           | Total inbound messages                           |\n| `nats.statistics.out.msgs`                   | Gauge   | -           | Total outbound messages                          |\n| `nats.statistics.in.bytes`                   | Gauge   | -           | Total inbound bytes                              |\n| `nats.statistics.out.bytes`                  | Gauge   | -           | Total outbound bytes                             |\n| `nats.statistics.dropped.count`              | Gauge   | -           | Messages dropped across all slow consumers       |\n| `nats.statistics.flush.counter`              | Gauge   | -           | Outgoing message flushes                         |\n| `nats.statistics.outstanding.requests`       | Gauge   | -           | Outstanding request count                        |\n| `nats.statistics.oks`                        | Gauge   | -           | Op `+OK` messages received                       |\n| `nats.statistics.errs`                       | Gauge   | -           | Op `-ERR` messages received                      |\n| `nats.statistics.exceptions`                 | Gauge   | -           |                                                  |\n| `nats.statistics.requests.sent`              | Gauge   | -           | Requests sent                                    |\n| `nats.statistics.replies.received`           | Gauge   | -           | Replies received                                 |\n| `nats.statistics.duplicate.replies.received` | Gauge   | -           | Duplicate replies received (advanced stats only) |\n| `nats.statistics.orphan.replies.received`    | Gauge   | -           | Orphan replies received (advanced stats only)    |\n\n\u003c/details\u003e\n\n## Testing\n\nAdd `natspring-starter-test` to your test dependencies:\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.malczuuu.natspring\u003c/groupId\u003e\n    \u003cartifactId\u003enatspring-starter-test\u003c/artifactId\u003e\n    \u003cversion\u003e{version}\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n```\n\n### Testcontainers\n\nThere is no official Testcontainers module for NATS. The community-maintained\n[`io.github.amadeusitgroup.testcontainers:nats`][nats-testcontainers-java] library provides a `NatsContainer`. Current\nlibrary integrates it with Spring Boot's `@ServiceConnection` for zero-config wiring.\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-testcontainers\u003c/artifactId\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.amadeusitgroup.testcontainers\u003c/groupId\u003e\n    \u003cartifactId\u003enats\u003c/artifactId\u003e\n    \u003cversion\u003e1.1.4\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n```\n\n```java\n@SpringBootTest\nclass MyIntegrationTests {\n\n  @Container @ServiceConnection\n  public static final NatsContainer nats = new NatsContainer(\"nats:2.14\");\n}\n```\n\n`@ServiceConnection` auto-configures `nats.server` from the running container - no manual property overrides needed.\n\n[nats-testcontainers-java]: https://github.com/AmadeusITGroup/nats-testcontainers-java\n\n## Building from source\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eExpand...\u003c/b\u003e\u003c/summary\u003e\n\nGradle **9.x+** requires **Java 17** or higher to run. For building the project, Gradle automatically picks up **Java\n25** via **toolchains** and the `foojay-resolver-convention` plugin. This Java version is needed because the project\nuses **ErrorProne** and **NullAway** for static nullness analysis.\n\nThe produced artifacts are compatible with **Java 17** thanks to `options.release = 17` in Gradle `JavaCompile` tasks.\nThis means that regardless of the Java version used to run Gradle, the resulting bytecode remains compatible.\n\nThe **default Gradle tasks** include `spotlessApply` (for code formatting) and `build` (for compilation and tests). The\nsimplest way to build the project is to run:\n\n```bash\n./gradlew\n```\n\n**Note** that the `natspring-integration-tests` module uses Testcontainers to spin up a real NATS server. **Docker must\nbe running** for these tests to pass.\n\n---\n\nTo **execute tests** use `test` task. Tests do not change `options.release` so newer Java API can be used.\n\n```bash\n./gradlew test\n```\n\n---\n\nTo **format the code** according to the style defined in [`build.gradle.kts`](./build.gradle.kts) rules use `spotlessApply`\ntask. **Note** that **building will fail** if code is not properly formatted.\n\n```bash\n./gradlew spotlessApply\n```\n\n**Note** that if the year has changed, add `-Pspotless.license-year-enabled` flag to update the year in license headers.\n\n```bash\n./gradlew spotlessApply -Pspotless.license-year-enabled\n```\n\n---\n\nTo **publish** the built artifacts to **local Maven repository**, use `publishToMavenLocal` task.\n\n```bash\n./gradlew publishToMavenLocal\n```\n\nNote that for using Maven Local artifacts in target projects, you need to add `mavenLocal()` repository.\n\n```kotlin\nrepositories {\n    mavenLocal()\n    mavenCentral()\n}\n```\n\n\u003c/details\u003e\n\n## License\n\nThis project is licensed under the Apache License, Version 2.0.\n\nThis project is not affiliated with, sponsored by, or endorsed by Spring Framework or NATS. All product names, logos,\nand brands are property of their respective owners.\n\n[maven-central]: https://central.sonatype.com/artifact/io.github.malczuuu.natspring/natspring-starter\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamianmalczewski%2Fnatspring","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdamianmalczewski%2Fnatspring","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamianmalczewski%2Fnatspring/lists"}