Lab 2.1: Navigate the OpenSearch Repository Structure
Background
The OpenSearch repository is large — hundreds of Gradle projects, tens of thousands of Java files.
A contributor who cannot navigate it quickly wastes hours and, worse, edits the wrong layer. This
lab is a guided tour: you will use ./gradlew projects, find, and grep to build a durable map
of the codebase, learn to locate any class in seconds, read a build.gradle to understand
inter-project dependencies, and recognize the SPDX header convention that every source file carries
(and that precommit enforces).
This is a reading and orientation lab. You will not change any code — but the muscle memory you build here is what makes every later lab fast.
Why This Lab Matters for Contributors
- "Where does this live?" is the question you answer dozens of times per PR. Answering it in seconds instead of minutes compounds.
- Editing the right layer matters: a fix in
:servervs:libs:corevs a:modules:*has different review owners, BWC implications, and test scopes. - Reading
build.gradledependency blocks tells you what you are allowed to call from where — and why some "obvious" imports are forbidden. - The SPDX header is a hard precommit gate; knowing it up front saves a failed CI run.
Prerequisites
- Lab 1.1 complete; you can run
./gradlew. - A clone of
opensearch-project/OpenSearch(your fork is fine; see Lab 2.2 for fork setup).
Step-by-Step Tasks
Step 1: The Top-Level Map
From the repo root, list directories and the Gradle project tree side by side:
ls -d */ | sort
./gradlew projects 2>/dev/null | sed -n '1,60p'
The directories you must know:
| Dir | Gradle project(s) | What lives there |
|---|---|---|
server/ | :server | The core engine. org.opensearch.*. The bulk of what you read and change. |
libs/ | :libs:core, :libs:common, :libs:x-content, :libs:geo, … | Shared low-level libraries with no dependency on :server. libs/core holds StreamInput/StreamOutput/Writeable (the wire primitives). |
modules/ | :modules:transport-netty4, :modules:lang-painless, :modules:analysis-common, :modules:reindex, … | Modules bundled in every distribution by default. |
plugins/ | :plugins:analysis-icu, :plugins:repository-s3, :plugins:discovery-ec2, … | Optional, in-repo plugins. (Security, k-NN, SQL, alerting, ml-commons live in separate repos.) |
client/ | :client:rest, :client:sniffer, :client:transport | Java clients. (The modern client is the separate opensearch-java repo.) |
distribution/ | :distribution:archives:*, :distribution:docker, :distribution:packages:* | Packaging: tarballs, Docker, deb/rpm, distribution/tools. |
test/framework/ | :test:framework | OpenSearchTestCase, OpenSearchIntegTestCase, InternalTestCluster, disruption helpers. |
qa/ | :qa:* | Cross-version BWC, rolling-upgrade, mixed-cluster, packaging QA. |
rest-api-spec/ | :rest-api-spec | REST API JSON specs and shared REST-YAML tests. |
buildSrc/, build-tools*/ | (build logic) | Gradle plugins and build conventions. |
sandbox/ | :sandbox:* | Experimental modules/plugins. |
Note:
:serveris allowed to depend onlibs/*, butlibs/*must not depend on:server— the dependency arrow only points one way. That is why core serialization primitives live inlibs/core: so everything (including:server) can use them. You will see this enforced by thebuild.gradlefiles in Step 5.
Step 2: Walk server/
:server is where you will spend most of your time. Get its internal shape:
ls server/src/main/java/org/opensearch | sort
The packages map directly to subsystems you will study:
| Package | Subsystem | Where it appears |
|---|---|---|
node | The Node object graph root | Level 1 |
rest | REST layer (RestController, BaseRestHandler) | Level 3, rest-layer deep dive |
action | Transport actions (the execution units) | Level 3, action-framework deep dive |
transport | Node-to-node transport | transport-layer deep dive |
cluster | Cluster state, coordination, routing, allocation | Level 4 |
indices, index | IndicesService, IndexShard, engine, translog, mapper | Level 6 |
search | Query/fetch phases, aggregations | Level 7 |
common | Utilities, settings, io, unit | everywhere |
# How big is the engine, roughly?
find server/src/main/java -name "*.java" | wc -l
# Where is each subsystem rooted?
find server/src/main/java/org/opensearch/index/engine -name "*.java" | head
Step 3: Source Sets — main vs test vs internalClusterTest
Each project has multiple source sets. Know which one a file is in before you edit it:
ls -d server/src/*/
# server/src/main/ -> production code
# server/src/test/ -> unit tests (OpenSearchTestCase, ...)
# server/src/internalClusterTest/ -> in-JVM multi-node integration tests (*IT)
This matters because the Gradle task differs (:server:test vs :server:internalClusterTest, from
Lab 1.2) and because production code must never depend on
test code.
Step 4: Locate Any Class
The core skill. Three reliable methods, fastest first:
# 1. By file name — when you know the class name:
find server -name "IndexShard.java" -path "*/main/*"
# server/src/main/java/org/opensearch/index/shard/IndexShard.java
# 2. By declaration — when you are not sure of the file name or want the exact site:
grep -rn "class TransportSearchAction" server/src/main/java | head
# 3. By usage — when you want callers of something:
grep -rn "applyIndexOperationOnPrimary" server/src/main/java | head
Practice until it is reflex. Find each of these and note the path:
for c in Node ClusterService RestController IndicesService IndexShard \
InternalEngine SearchService StreamInput Writeable; do
echo "== $c =="
find server libs -name "$c.java" -path "*/main/*"
done
Note that StreamInput and Writeable resolve under libs/core, not server — the wire
primitives live in the shared library, exactly as Step 1 predicted.
Step 5: Read a build.gradle for Dependencies
A project's build.gradle declares what it may depend on. This is how you learn the allowed
direction of imports. Read the server's:
sed -n '1,80p' server/build.gradle
grep -nE "api project|implementation project|testImplementation project" server/build.gradle
You will see :server declaring dependencies on :libs:core, :libs:common, :libs:x-content,
and the Lucene artifacts — and you will not see any libs/* project depending on :server. Now
read a library's to confirm the one-way arrow:
grep -nE "project\(" libs/core/build.gradle
# libs:core depends only on other libs / external jars — never on :server
This dependency structure is why certain code lives where it does. A Writeable in libs/core can
be used by :server, every module, and every plugin; if it lived in :server, the libraries below
it could not use it. Keep this in mind when a reviewer says "this belongs in libs, not server."
Tip: To see the resolved dependency graph (including transitive Lucene/Netty/etc.):
./gradlew :server:dependencies --configuration compileClasspath | head -60.
Step 6: The SPDX Header Convention
Every OpenSearch source file carries an SPDX license header. New files must include it or precommit fails. Inspect a real one:
head -20 server/src/main/java/org/opensearch/index/shard/IndexShard.java
The canonical header for a new file is:
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
Older files that predate the fork also carry an Apache-2.0 attribution block referencing the original Elasticsearch copyright (the fork preserved upstream attribution). You do not remove those. For any new file you create (e.g. a new test in Lab 2.3), copy the SPDX header from a sibling file in the same directory.
Confirm precommit cares:
grep -rn "licenseHeaders\|forbiddenApis\|spotless" server/build.gradle build-tools*/ buildSrc/ 2>/dev/null | head
Step 7: rest-api-spec/ and qa/ — Know They Exist
Two directories you will not edit yet but must recognize when reading PRs:
ls rest-api-spec/src/main/resources/rest-api-spec/api | head
# the JSON specs describing every REST endpoint (params, paths, bodies)
ls rest-api-spec/src/yamlRestTest 2>/dev/null
ls qa/ | head
# bwc-test, rolling-upgrade, mixed-cluster, full-cluster-restart, ...
rest-api-spec is the contract for the REST API and the home of shared YAML REST tests; qa/ holds
the cross-version backward-compatibility tests you will write in Level 9.
A PR that changes a REST endpoint usually touches rest-api-spec; a PR that changes the wire format
usually adds a qa/ BWC test.
Implementation Requirements
This lab produces a map, not code. Deliverables:
- A filled-in copy of the top-level dir → Gradle project → purpose table, verified against your own
./gradlew projectsoutput. - The file path (from memory, then verified) for:
Node,IndexShard,InternalEngine,SearchService,StreamInput,Writeable. - Two sentences explaining why
StreamInput/Writeablelive inlibs/coreand not:server. - A note of the
:serverproject dependencies you found inserver/build.gradle. - The SPDX header pasted from a real file, plus the path you copied it from.
Troubleshooting
./gradlew projects is overwhelming
Pipe it. ./gradlew projects | grep -E "':(server|libs|modules):" narrows to the projects you care
about. You rarely need the full list at once.
find returns test and main copies of the same class name
Constrain the path: add -path "*/main/*" for production code or -path "*/test/*" for tests. Many
classes have a FooTests.java next to Foo.java.
grep -rn "class X" returns nothing
The class may be nested, generic, or named differently than you assume. Try grep -rn "X" --include=*.java -l
to find files mentioning it, or search the declaration loosely: grep -rn "class X\b\|interface X\b\|enum X\b".
A new file fails precommit with a license-header error
You omitted the SPDX header. Copy it verbatim from a sibling file in the same package. Then re-run
./gradlew precommit.
Expected Output
Your locate-the-class drill should produce paths like:
== Node == server/src/main/java/org/opensearch/node/Node.java
== IndexShard == server/src/main/java/org/opensearch/index/shard/IndexShard.java
== InternalEngine == server/src/main/java/org/opensearch/index/engine/InternalEngine.java
== SearchService == server/src/main/java/org/opensearch/search/SearchService.java
== StreamInput == libs/core/src/main/java/org/opensearch/core/common/io/stream/StreamInput.java
== Writeable == libs/core/src/main/java/org/opensearch/core/common/io/stream/Writeable.java
(Exact libs/core sub-paths vary by branch — the point is they are under libs/core, not server.)
Stretch Goals
-
Map a module's extension points. Open
modules/analysis-common/build.gradleand its*Plugin.java; identify whichPlugininterfaces it implements (you will study these in Level 3 and the plugin-architecture deep dive). -
Trace a REST route to its handler. Pick an endpoint from
rest-api-spec(say_count), thengrep -rn "_count" server/src/main/java/org/opensearch/restto find the handler that registers it. This is the bridge to Level 3, Lab 3.1. -
Find every place a setting is defined. Settings are
Setting<T>constants. Rungrep -rn "Setting.intSetting\|Setting.boolSetting" server/src/main/java/org/opensearch/cluster | headto see how cluster settings are declared. -
Diff the dependency graph of two projects. Compare
./gradlew :libs:core:dependencieswith./gradlew :server:dependenciesand articulate why the library's graph is a strict subset of the server's.
Coding Exercises
A "reading" lab still produces code — here you build the navigation tooling a fluent contributor
keeps in their pocket. Each script must run against a real OpenSearch checkout. Use rg/find
exclusively to locate things; never bake in a line number.
-
(warm-up) A module-map generator. Write
module-map.shthat prints, for each top-level source dir (server libs modules plugins client), the directory name, its Gradle project path, and the count of*.javafiles under it (find <dir> -name '*.java' | wc -l). Verify: the output reproduces the dir → project → purpose table from Step 1 with real file counts, and the script handles a missing dir gracefully (some branches lacksandbox/). -
(warm-up) A "find any class" wrapper. Write
findclass.sh <ClassName>that tries, in order: (1)find ... -name "<ClassName>.java" -path "*/main/*", then (2)grep -rn "class <ClassName>\b\|interface <ClassName>\b\|enum <ClassName>\b" --include=*.java, printing whichever hits first and labeling which method found it. Verify:findclass.sh IndexShard,findclass.sh StreamInput, andfindclass.sh Writeableeach return the path the lab's Expected Output shows — andStreamInput/Writeableresolve underlibs/core, notserver. -
(core) A dependency-direction checker. Write
check-dep-direction.shthat greps everylibs/*/build.gradlefor aproject(":server")dependency and fails (exit 1) if any library declares one — encoding the one-way arrow from Step 5 as an executable rule. Verify: on a clean checkout it exits0; if you add a fakeimplementation project(':server')line tolibs/core/build.gradlein a scratch branch, it exits1and names the offending file. -
(core) A settings-inventory tool. Settings are
Setting<T>constants. Write a script thatrgsSetting\.(int|bool|long|byteSize|time|simpleString)Settingacrossserver/src/main/java/org/opensearch/clusterand prints a sorted, de-duplicated list of the setting keys (the first string argument). Verify: the list is non-empty and includes a setting you can also see in_cluster/settingsfrom a running node (Lab 1.3). This generalizes Stretch Goal 3 into a real inventory. -
(core) A SPDX-header auditor. Write
spdx-audit.shthat finds every*.javaunder a given directory whose first 20 lines do not containSPDX-License-Identifier, printing the offenders. Verify: onserver/src/main/javait prints nothing (all files carry the header); create a new.javafile without the header in a scratch dir and confirm the auditor flags it — the same gate./gradlew precommitenforces (Step 6). -
(advanced) Advanced challenge — a code-spelunking tool that traces a route to its action. Write
trace-endpoint.sh <path-fragment>(e.g._count) that: (1) finds theRest*Actionwhoseroutes()registers a matchingnew Route(, viarg; (2) reads that handler to find the*Request/*Actionit builds; (3)rgs for the matchingTransport*Actionclass declaration; and (4) prints the chainREST handler → request → transport actionwith file paths. Verify: for_countit printsRestCountAction → ... → TransportSearchAction-family (confirm the real chain on your branch — names drift), and for_searchit printsRestSearchAction → TransportSearchAction. This is the bridge to Level 3, Lab 3.1: you have automated the trace you will do by hand there.
Issues to Practice On
The skills here — locating code, reading build.gradle, knowing the right layer — are exactly what
let you triage issues, so this lab pairs naturally with issue triage on
opensearch-project/OpenSearch.
# Untriaged issues are where navigation skill pays off — you find where the bug lives:
gh issue list --repo opensearch-project/OpenSearch --label "untriaged" --state open
gh issue list --repo opensearch-project/OpenSearch --label "good first issue" --state open
# Area labels point you at a layer (labels move; confirm on the tracker):
gh issue list --repo opensearch-project/OpenSearch --label "Storage" --state open
gh issue list --repo opensearch-project/OpenSearch --label "Cluster Manager" --state open
gh label list --repo opensearch-project/OpenSearch | grep -iE "search|cluster|storage|index|plugin"
Representative issue patterns for this subsystem:
- An "untriaged" issue that needs locating. The triage skill is navigation: read the report,
reproduce, then use your
findclass.sh/trace-endpoint.shtooling to pinpoint the responsible class and propose the right label/area. A high-quality triage comment ("this lives inserver/.../X.java, the relevant method isY, likely areaSearch") is itself a valued contribution and often the first step a maintainer asks for. - A "wrong layer" question. Issues sometimes propose a fix in
:serverthat actually belongs inlibs/*(or a module). Use the dependency-direction check from Exercise 3 to argue where it should live, citing thebuild.gradleevidence.
Planted-bug drill. Break the layering rule and watch the build catch it:
- In a scratch branch, add
implementation project(':server')tolibs/core/build.gradleand run./gradlew :libs:core:compileJava. Watch Gradle either reject the circular dependency or surface it. Revert, and confirm your Exercise-3 checker would have flagged it before you ran Gradle. - Then a header plant: create a new
.javafile underserver/src/main/java/...without the SPDX header, run./gradlew :server:precommit(or thelicenseHeaderstask), and watch it fail. Note the exact task name in the error. Fix by copying the header from a sibling. This is the gate every new file in Lab 2.3 must pass.
Etiquette: triage and PRs alike — claim/comment before working, reproduce first, and any code PR
needs a test + CHANGELOG + DCO Signed-off-by (git commit -s; Lab 2.2).
Triage etiquette and tone: community interaction.
Validation / Self-check
You are done when you can answer these without notes:
- Which top-level dir holds the core engine, and which holds the shared wire-serialization primitives? Why are they separate?
- In which direction may dependencies point between
:serverandlibs/*? How do you verify it from abuild.gradle? - Give the three ways to locate a class, and which you reach for when you know only the behavior, not the name.
- What are the three source sets of
:server, and which Gradle test task runs each? - What must every new
.javafile contain to pass precommit, and where do you copy it from? - What is
rest-api-spec/for, and what doesqa/hold?