Coding conventions
The guidelines help to encourage consistency and best practices among people working on Apache Pulsar code base. You should observe the guidelines unless there is compelling reason to ignore them.
The canonical coding reference for the Pulsar master branch is CODING.md in the apache/pulsar repository. It is the always-up-to-date version of these conventions and covers each topic in more depth, with code examples. This page summarizes the key points.
Some conventions below — most notably slog logging — apply to the master branch only. When contributing to a maintenance branch, follow the conventions of the surrounding code in that branch.
Java code style
Apache Pulsar code follows the Sun Java Coding Convention, with the following additions.
- Indentation should be 4 spaces. Tabs should never be used.
- Use curly braces even for single-line ifs and elses.
- No @author tags in any javadoc.
- Prefer imports over fully qualified class names in code. Use a fully qualified class name only when needed to disambiguate a name collision that imports cannot resolve.
- Every TODO must reference a GitHub issue, e.g.
// TODO: https://github.com/apache/pulsar/issues/XXXX.
Pulsar uses checkstyle to enforce coding style, refer to the checkstyle rules for all enforced checkstyle rules. Lombok is enabled in the codebase.
Logging
See CODING.md → Logging for the details.
- On
master, prefer slog (io.github.merlimat.slog) via Lombok's@CustomLog. SLF4J is deprecated for new code; never useSystem.outorSystem.err. - Default new logs to
TRACE/DEBUG, notINFO— Pulsar overusesINFOand floods production logs. ReserveINFOfor low-frequency lifecycle and state-change events. UseDEBUGin a way where it could be enabled in production without causing too many log entries; useTRACEfor more detailed information. - Attach data as structured attributes —
log.info().attr("topic", topic).log("Published")— not interpolated into the message string. - For expensive
DEBUG/TRACEvalues, don't guard withisDebugEnabled()/isTraceEnabled(); use slog's lazy form —log.debug().attr("dump", () -> expensiveDump()).log("..."). - Avoid logging on hot paths, and stack traces at
INFOor lower.
Asynchronous programming
See CODING.md → Asynchronous programming for the details and code examples.
CompletableFutureis preferred over Guava'sListenableFuturefor new code.- A method returning
CompletableFuturemust not throw synchronously. Propagate failures — including argument-validation failures — through the returned future withCompletableFuture.failedFuture(e). - Never block on event-loop / async-execution threads — no
Thread.sleep,Future.get(),CompletableFuture.join(), or blocking IO. - Avoid nested futures (
CompletableFuture<CompletableFuture<T>>); flatten withthenCompose. PreferOrderedExecutorfor ordered asynchronous work. - Limit concurrency and handle backpressure when firing many async operations.
Dependencies
Prefer existing dependencies over new libraries. Pulsar commonly uses Apache Commons / Guava (utilities), FastUtil (type-specific collections), JCTools (concurrent structures), RoaringBitmap (compressed bitsets), Caffeine (caching), Jackson (JSON), Prometheus / OpenTelemetry (metrics), and Netty (networking and buffers).
A new dependency must be justified (why existing ones are insufficient), and dependencies are bundled with the binary distributions, so the bundled-dependency LICENSE/NOTICE files must be updated — verify with ./gradlew checkBinaryLicense. See License header.
Resource and memory management
- Always close resources (streams, connections, executors, buffers) — prefer try-with-resources.
- On internal networking/messaging paths, prefer Netty
ByteBufoverjava.nio.ByteBufferunless an external API requires it; release ref-counted buffers you allocate. - Don't hand-optimize allocation away — Pulsar runs on ZGC, where short-lived allocations are cheap. Don't add new Netty
Recyclerusage; seeCODING.md→ Resource and memory management.
Monitoring
- Any new features should come with appropriate metrics, so monitoring the feature is working correctly.
- Those metrics should be taken seriously and only export useful metrics that would be used on production on monitoring/alerting healthy of the system, or troubleshooting problems.
Testing conventions
See CODING.md → Testing conventions for the details.
- Tests use TestNG + Mockito. Prefer AssertJ assertions (with descriptions) over TestNG asserts; use Awaitility for async conditions instead of
sleeptiming, with timeouts to prevent hangs. - Every feature or bug fix needs deterministic tests for edge and failure cases. A bug-fix test must fail on the unpatched code for the real reason.
- No reflection into private state (
WhiteboxImpl,setAccessible(true)). Expose a package-private@VisibleForTestingaccessor and put the test in the same package. - New integration-style tests should extend
SharedPulsarBaseTest, usegetNamespace()andnewTopicName(), and never hardcode namespace/topic names. - Close and release what the test allocates; a
ByteBufleak detected by the pooled allocator is a real bug. - Validate performance optimizations with a JMH benchmark under
microbench/.
For how to run tests (scoping with --tests, test groups, integration tests), see Setup and building and CONTRIBUTING.md → Running tests.
Configuration
- When adding configuration options, use clear, descriptive names and provide sensible defaults.
- If you run the program without tuning parameters, use the default values.
- All configuration settings should be added accordingly in the default configuration file directory and documented accordingly.
Concurrency
See CODING.md → Concurrency for the details, including the Java-Memory-Model guidance.
Apache Pulsar is a low latency system, it is implemented as a purely asynchronous service, which is accomplished as follows:
- All public classes should be thread-safe. If a class is not thread-safe, it should be annotated @NotThreadSafe.
- Protect shared mutable state; prefer fine-grained synchronization. Prefer the single-writer principle (a given piece of state mutated by only one thread) to avoid concurrent mutation entirely.
- Minimize work while holding a lock — never call out to listener/callback code while holding a lock.
- Prefer using OrderedExecutor for executing any asynchronous actions. The mutations to the same instance should be submitted to the same thread to execute.
- All threads should have proper meaningful name. When creating thread pools, prefer Netty's
io.netty.util.concurrent.DefaultThreadFactory. - Prefer immutable objects, and conform to the Java Memory Model — a field accessed by more than one thread needs explicit visibility (
volatile, or the same lock guarding every read and write).
Backwards compatibility
See CODING.md → Backward compatibility for the details.
- Changes must not break public APIs, client compatibility, wire-protocol compatibility, or serialized/metadata formats. The servers MUST be able to support requests from both old and new clients simultaneously to enable no-downtime upgrades.
- Plugin / SPI extension points are public API — interfaces selected by a
*ClassNameconfiguration setting (e.g.AuthorizationProvider,EntryFilter,BrokerInterceptor) have third-party implementations. Changing such an interface generally needs a PIP and must not land in maintenance-branch backports. - Introduce new or changed behaviour behind a backward-compatible default — opt-in via configuration rather than silently changing existing deployments.