skip to content

How would you decide between `java`, `java-library`, and `application` across a multi-module build, and what do they contribute respectively?

level: seniorimportance: should knowfreq 55%

answer

  1. all apply/share java base
  2. java = plain, no api
  3. java-library adds api → compile avoidance
  4. application adds run + distribution
  5. encode defaults in convention plugins

basics

~20 s

Use java for a basic compilable module, java-library for a reusable library that other modules depend on (it adds api for public deps), and application for an executable entry-point module (adds run + a distribution). The latter two both apply java.

solid answer

~50 s

All three are JVM language plugins that share the `java` foundation — source sets, `compileJava`, `test`, `jar` — but they signal different module roles. **`java`** is the bare minimum: a module that compiles and tests but has no notion of a public API surface or a runnable entry point. **`java-library`** adds the `api` configuration so a *reusable library* can separate public dependencies (visible on consumers' compile classpath) from internal `implementation` ones, enabling compile avoidance — this is what you apply to most internal library modules so changes don't needlessly recompile the whole graph. **`application`** (also applies `java`) makes a module *executable*: it adds a `run` task, the `mainClass` setting, and `distZip`/`installDist` packaging. In a typical layout you give leaf service/app modules `application`, the shared library modules `java-library`, and use bare `java` rarely (e.g. a build-internal helper). Mixing roles cleanly keeps public surfaces small and builds fast.

code

kotlin · 11 lines
kotlin
// :app — runnable leaf
plugins { application }
application { mainClass.set("com.example.App") }
dependencies { implementation(project(":service")) }

// :service — reusable library
plugins { `java-library` }
dependencies {
    api("com.fasterxml.jackson.core:jackson-annotations:2.17.0")
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
}

go deeper

for a junior

Match the plugin to the role: library vs runnable vs plain; know each applies java.

for a middle

Explain the api distinction in java-library and the run/distribution additions in application.

for a senior

Reason about compile avoidance, encapsulation, and which modules get which plugin in a real multi-module graph.

for a principal

Standardize these choices via convention plugins and govern public-API surfaces and toolchains across the org.

## The shared base All three plugins ultimately apply `java`, so every one of them contributes: - the `main` and `test` **source sets**, - compile/resource/test tasks (`compileJava`, `processResources`, `test`), - the `jar` task and lifecycle wiring (`assemble`/`check`/`build`). What differs is the *role* each plugin expresses. ## `java` — a plain module Use it when a module just needs to compile and be tested but isn't a published library and isn't runnable. It offers `implementation`/`compileOnly`/`runtimeOnly` but **no `api`**, so it can't express a public-API dependency surface. In practice it's the least common choice in a well-factored multi-module build. ## `java-library` — a reusable library Applies `java` and adds the **`api`** configuration. This lets the module declare which dependencies are part of its **public contract** (on consumers' compile classpath via the `apiElements` variant) versus internal (`implementation`, runtime-only for consumers). The payoff is **compile avoidance** — changing an internal dependency or an `implementation`-only ABI doesn't recompile downstream modules — plus encapsulation. Apply this to most shared modules others depend on. ## `application` — an executable Applies `java` and adds: - the `application {}` extension (`mainClass`, default JVM args), - the `run` task, - distribution tasks (`installDist`, `distZip`, `distTar`) with generated start scripts. Apply this to the *leaf* module that is actually launched — a CLI, a service entry point. ## Putting it together ```kotlin // :app (the runnable leaf) plugins { application } application { mainClass.set("com.example.App") } dependencies { implementation(project(":service")) } // :service (reusable library) plugins { `java-library` } dependencies { api("com.fasterxml.jackson.core:jackson-annotations:2.17.0") // in public signatures implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") } ``` ## Decision checklist - Is the module **launched** (has a `main`)? → `application`. - Is it a **library consumed by other modules** and needs to express public deps? → `java-library`. - Is it neither (a tiny internal helper, or you genuinely don't care about `api`)? → `java`. ## Org-level concern At scale you'd encode these defaults in **convention plugins** (e.g. `my.java-library-conventions`) so every module applies the right base, toolchain, and test setup consistently rather than copy-pasting.

  • Why not just apply `java-library` everywhere, including the runnable module?
    You can apply both, but the runnable module needs `application` for `run`/distribution. `java-library` is about expressing a public API, which a leaf executable usually doesn't need to publish.
  • How do you keep these choices consistent across dozens of modules?
    Author convention plugins (in `buildSrc` or an included build) like `java-library-conventions` that apply the right base plugin, toolchain, and shared test config, then apply those by name in each module.
  • Can a module be both a library and runnable?
    Yes — apply both `java-library` and `application`. But that's a smell unless the module genuinely is a published library that also ships a CLI; usually you'd split them.

saying these in an interview costs you the question

  • Saying `application` doesn't include what `java` provides (it applies `java`).
  • Recommending `api` for everything 'to be safe', which kills compile avoidance.
  • Claiming bare `java` supports the `api` configuration.

context