Open-Source Wikis

/

Elasticsearch

/

How to contribute

/

Patterns and conventions

elastic/elasticsearch

Patterns and conventions

The Elasticsearch codebase is large and old enough that consistent patterns matter more than individual taste. This page lists the recurring shapes you will see.

Naming

Pattern Convention
REST handlers Rest<Action>Action extends BaseRestHandler
Transport actions Transport<Action>Action extends HandledTransportAction (or *MasterNodeAction, *NodesAction, *BroadcastByNodeAction, *SingleShardAction)
ActionType strings <scope>:<sub-scope>/<verb> — e.g. indices:data/read/search, cluster:admin/snapshot/create, internal:cluster/coordination/join
Test classes <ClassName>Tests (unit), <ClassName>IT (integration)
Setting keys dotted lowercase, scoped (e.g. cluster.routing.allocation.disk.threshold_enabled)
Plugins one class extending Plugin plus interface implementations (ActionPlugin, SearchPlugin, etc.)

The action-name scope is consumed by the privilege resolver (x-pack/plugin/security) to decide which built-in role can call which action. New action names must follow the convention to integrate cleanly.

Action layer (transport actions)

Almost every operation is split into:

  1. A RequestBuilder/Request extending ActionRequest (Writeable + ToXContent).
  2. A Response extending ActionResponse (Writeable + ToXContent).
  3. A TransportAction doing the real work.
  4. A RestAction translating HTTP to the action.
  5. (optional) A Client convenience method on IndicesAdminClient / ClusterAdminClient.

The base classes prefer composition over inheritance. Prefer HandledTransportAction for "do it on this node" actions, TransportMasterNodeAction when the work must run on the elected master, TransportNodesAction for fan-out-to-all-nodes, TransportSingleShardAction for read-from-one-shard work, and TransportReplicationAction for primary+replica writes.

Concurrency: ActionListener everywhere

The project does not use CompletableFuture or reactive types in production code paths. Instead, every async API is callback-driven and takes a final ActionListener<T> parameter:

indicesService.cluster().syncedFlush(req, new ActionListener<>() {
    @Override public void onResponse(SyncedFlushResponse r) { ... }
    @Override public void onFailure(Exception e) { ... }
});

Common helpers:

  • ActionListener.wrap(onResponse, onFailure) — lambda factory.
  • ActionListener.delegateFailure(...) — chain a synchronous handler that may throw.
  • RefCountingListener — multi-shot listener used to fan out and join.
  • SubscribableListener — listener that can be subscribed to multiple times; lets producers and consumers stay decoupled.
  • ActionListener.runAfter(...) — run a finalizer regardless of success/failure.

ActionListener.completeWith(listener, () -> compute()) is the safe way to invoke a checked supplier on the listener's behalf.

Threading

Never spawn raw threads. Use ThreadPool (server/src/main/java/org/elasticsearch/threadpool/ThreadPool.java) and submit work to a named pool:

Pool Purpose
generic Catch-all for short tasks.
search Query and fetch phases.
search_throttled Search on frozen / throttled indices.
write Indexing / bulk path.
refresh Lucene refresh tasks.
flush Lucene flush + translog sync.
force_merge Manual force-merge.
management Admin-y tasks (settings updates, etc.).
snapshot / snapshot_meta Snapshot creation/restore.
system_read / system_write / system_critical_read / system_critical_write System index access (must not block on user data).
analyze Analyzer-only requests.
cluster_coordination Coordinator messages.

Long-running tasks should yield by re-submitting themselves; the project has helpers like AbstractRunnable, ContextPreservingActionListener, and EsExecutors.newScaling/newFixed.

Cluster state updates

Cluster state mutations go through ClusterStateTaskExecutor + ClusterService.submitUnbatchedStateUpdateTask (or, preferably, a batched executor). The executor is a pure function (currentState, tasks) -> newState. Never modify cluster state outside of an executor; never block on a cluster-state-updating thread.

clusterService.submitUnbatchedStateUpdateTask("update-foo", new ClusterStateUpdateTask() {
    @Override public ClusterState execute(ClusterState current) {
        return ClusterState.builder(current).metadata(...).build();
    }
});

Serialization (Writeable + transport versions)

All over-the-wire types implement Writeable (and usually ToXContentObject). Wire compatibility is enforced via transport versions: every breaking format change introduces a new TransportVersion constant referenced from writeTo/readFrom.

Adding a new field to a Writeable:

  1. Add a public static final TransportVersion MY_DESCRIPTIVE_NAME = TransportVersion.fromName("my_descriptive_name") in server/src/main/java/org/elasticsearch/TransportVersions.java.
  2. Confirm the backport branches.
  3. Run ./gradlew generateTransportVersion to regenerate server/src/main/resources/transport/upper_bounds/*.csv.
  4. In the new code path, gate the new field with if (out.getTransportVersion().onOrAfter(MY_DESCRIPTIVE_NAME)).

Index-state changes follow a parallel pattern with IndexVersion (server/src/main/java/org/elasticsearch/index/IndexVersions.java).

XContent (JSON / YAML / CBOR / Smile)

The project's polymorphic content layer is XContent (libs/x-content). Types implement ToXContent for serialization and accept XContentParser for parsing. Never call Jackson APIs directly — always go through XContentBuilder / XContentParser. The parser respects the four content types Elasticsearch supports.

Logging

private static final Logger logger = LogManager.getLogger(MyClass.class);

logger.debug("operation [{}] for index [{}]", op, index);
logger.warn(() -> Strings.format("expensive: %s", computeExpensive()), ex);

Rules:

  • Always parameterized; never string concatenation.
  • For TRACE/DEBUG, wrap expensive message construction in a Supplier<String> (() -> Strings.format(...)).
  • Use Elasticsearch's own logger interfaces (org.elasticsearch.logging.Logger for libs that must not depend on log4j directly).
  • Reserve ERROR for unrecoverable conditions; prefer WARN for things an admin should investigate.

Releasable / RefCounted

Many object lifetimes outlive a single method call (Lucene readers, transport response handles, blob store streams). They implement:

  • Releasable — single-shot close().
  • RefCountedincRef() / tryIncRef() / decRef(); closed when ref count hits zero.

The codebase relies on try-with-resources and IOUtils.close(...) heavily. New code should adopt the same pattern.

Error handling

  • Don't swallow exceptions.
  • Wrap with ElasticsearchException, ElasticsearchStatusException, etc. to propagate REST status codes.
  • Avoid logging client-caused exceptions; let the API response carry the error.
  • For shard-level failures use ShardOperationFailedException and let the action layer aggregate.

Backwards compatibility

The repo supports rolling upgrades within a major and full-cluster restarts across one major. Practical implications:

  • Writeable changes need transport versions (above).
  • New REST endpoints / parameters need a RestApiVersion annotation if they change pre-existing behavior.
  • New cluster state metadata needs a Custom registration with a TransportVersion minimum.
  • Index settings: prefer additive changes; never remove a setting in a minor release.

Settings

  • Setting<T> is the type. Properties: NodeScope, IndexScope, Dynamic, Filtered (for secrets), Final.
  • Cluster settings are registered via Plugin#getSettings; index settings via IndexScopedSettings.
  • Secure settings (passwords, certificates) live in the keystore (SecureSetting).
  • Reading dynamic settings: subscribe via ClusterSettings.addSettingsUpdateConsumer(...).

License headers

  • Default header (everywhere except x-pack/): tri-license — Elastic License 2.0 / SSPL 1 / AGPL v3.
  • x-pack/-only: Elastic License 2.0.

Copy the header verbatim from a neighboring file. The IDE templates in CONTRIBUTING.md insert the right one based on path.

Tests reflect production patterns

Tests must use the same abstractions: don't bypass ActionListener to use a synchronous facade, don't construct IndexShard from scratch when IndicesService.indexShard(...) will do.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Elasticsearch wiki | Factory