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:
- A
RequestBuilder/RequestextendingActionRequest(Writeable+ToXContent). - A
ResponseextendingActionResponse(Writeable+ToXContent). - A
TransportActiondoing the real work. - A
RestActiontranslating HTTP to the action. - (optional) A
Clientconvenience method onIndicesAdminClient/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:
- Add a
public static final TransportVersion MY_DESCRIPTIVE_NAME = TransportVersion.fromName("my_descriptive_name")inserver/src/main/java/org/elasticsearch/TransportVersions.java. - Confirm the backport branches.
- Run
./gradlew generateTransportVersionto regenerateserver/src/main/resources/transport/upper_bounds/*.csv. - 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 aSupplier<String>(() -> Strings.format(...)). - Use
Elasticsearch's own logger interfaces (org.elasticsearch.logging.Loggerfor libs that must not depend on log4j directly). - Reserve
ERRORfor unrecoverable conditions; preferWARNfor 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-shotclose().RefCounted—incRef()/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
ShardOperationFailedExceptionand let the action layer aggregate.
Backwards compatibility
The repo supports rolling upgrades within a major and full-cluster restarts across one major. Practical implications:
Writeablechanges need transport versions (above).- New REST endpoints / parameters need a
RestApiVersionannotation if they change pre-existing behavior. - New cluster state metadata needs a
Customregistration with aTransportVersionminimum. - 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 viaIndexScopedSettings. - 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.