Skip to main content

Gradle

Gradle is the other major build tool in the Java ecosystem. It is newer than Maven, uses code (Kotlin or Groovy) instead of XML for configuration, and is generally faster thanks to incremental builds and a build cache. This chapter shows how to set up the same REST API project with Gradle using the Kotlin DSL (build.gradle.kts).

Maven vs Gradle​

FeatureMavenGradle
ConfigurationXML (pom.xml)Kotlin/Groovy (build.gradle.kts)
SpeedGoodFaster (incremental builds, daemon, cache)
FlexibilityConvention-based, less customizableHighly customizable
Learning curveLower (XML is declarative)Moderate (Kotlin DSL is a real language)
EcosystemLargest, most enterprise projectsGrowing, default for Android/Kotlin
IDE supportExcellent in all IDEsExcellent in IntelliJ, good in others

Neither is "better" - pick whichever your team or project uses. Both solve the same problems.

Installing Gradle​

macOS (Homebrew)​

brew install gradle

Windows​

Download from gradle.org, extract, and add the bin/ directory to your PATH.

curl -s "https://get.sdkman.io" | bash
sdk install gradle

Verify​

gradle --version

Result:

------------------------------------------------------------
Gradle 8.10
------------------------------------------------------------
Build time: 2024-08-14
Kotlin: 1.9.24
Groovy: 3.0.22
JVM: 21.0.2

Creating a Gradle project​

Gradle follows the same directory convention as Maven:

task-api-gradle/
├── build.gradle.kts # Build configuration (Kotlin DSL)
├── settings.gradle.kts # Project settings
└── src/
├── main/
│ └── java/
│ └── taskapi/
│ ├── Task.java
│ ├── TaskStore.java
│ ├── TaskHandler.java
│ └── ApiServer.java
└── test/
└── java/
└── taskapi/

settings.gradle.kts​

The settings file names the project:

rootProject.name = "task-api"

build.gradle.kts​

The build file replaces Maven's pom.xml:

plugins {
java
application
id("com.gradleup.shadow") version "8.3.5"
}

group = "com.example"
version = "1.0.0"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
}

dependencies {
// Jackson for JSON processing
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}

application {
mainClass = "taskapi.ApiServer"
}

tasks.shadowJar {
archiveClassifier = "all"
}

Compare this to the Maven pom.xml:

  • Shorter - Kotlin DSL is more concise than XML
  • plugins { java; application } - declares this is a Java application
  • repositories { mavenCentral() } - where to download dependencies (same Maven Central)
  • dependencies { implementation(...) } - equivalent to Maven's <dependency> block
  • application { mainClass = ... } - enables gradle run to start the app
  • shadowJar task - the Shadow plugin creates a fat JAR without the common edge cases of manual zipTree packaging

Gradle vs Maven dependency notation​

Maven:
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>

Gradle:
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")

The compact group:artifact:version string is much easier to read.

Dependency configurations​

GradleMaven equivalentMeaning
implementationcompileCompile + runtime, not exposed to consumers
apicompileCompile + runtime, exposed to consumers
testImplementationtestTest compile + runtime only
compileOnlyprovidedCompile only, not in JAR
runtimeOnlyruntimeRuntime only

Improving the REST API with Jackson​

In the Maven chapter we used Gson. Here we use Jackson - the other popular JSON library - to show both options. Jackson is the industry default for most Java projects.

Updated TaskHandler.java with Jackson​

The source files (Task.java, TaskStore.java, ApiServer.java) are identical to the Maven chapter. Only TaskHandler.java changes to use Jackson instead of Gson:

// src/main/java/taskapi/TaskHandler.java
package taskapi;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;

public class TaskHandler implements HttpHandler {

private final TaskStore store;
private final ObjectMapper mapper = new ObjectMapper();

public TaskHandler(Path dataFile) {
this.store = new TaskStore(dataFile);
}

@Override
public void handle(HttpExchange exchange) throws IOException {
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();

try {
if (path.equals("/api/tasks")) {
switch (method) {
case "GET" -> handleListTasks(exchange);
case "POST" -> handleCreateTask(exchange);
default -> sendError(exchange, 405, "Method not allowed");
}
} else if (path.startsWith("/api/tasks/")) {
int id = parseIdFromPath(path);
switch (method) {
case "GET" -> handleGetTask(exchange, id);
case "PUT" -> handleUpdateTask(exchange, id);
case "DELETE" -> handleDeleteTask(exchange, id);
default -> sendError(exchange, 405, "Method not allowed");
}
} else if (path.equals("/api/health") && method.equals("GET")) {
sendJson(exchange, 200, new HealthResponse("ok"));
} else {
sendError(exchange, 404, "Not found: " + path);
}
} catch (IllegalArgumentException e) {
sendError(exchange, 400, e.getMessage());
} catch (Exception e) {
System.err.println("Internal error: " + e.getMessage());
sendError(exchange, 500, "Internal server error");
}
}

// ── Handlers ──────────────────────────────────────────────

private void handleListTasks(HttpExchange exchange) throws IOException {
sendJson(exchange, 200, store.load());
}

private void handleGetTask(HttpExchange exchange, int id) throws IOException {
Task task = findTaskOrNull(id);
if (task == null) {
sendError(exchange, 404, "Task not found: " + id);
return;
}
sendJson(exchange, 200, task);
}

private void handleCreateTask(HttpExchange exchange) throws IOException {
String body = readRequestBody(exchange);

// Jackson's tree model for flexible parsing
JsonNode node = mapper.readTree(body);
JsonNode descNode = node.get("description");

if (descNode == null || descNode.asText().isBlank()) {
sendError(exchange, 400, "Description is required");
return;
}

String description = descNode.asText();
List<Task> tasks = store.load();
int nextId = tasks.stream().mapToInt(Task::id).max().orElse(0) + 1;
Task task = new Task(nextId, description, false);
tasks.add(task);
store.save(tasks);

sendJson(exchange, 201, task);
}

private void handleUpdateTask(HttpExchange exchange, int id)
throws IOException {
List<Task> tasks = store.load();
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i).id() == id) {
Task completed = tasks.get(i).complete();
tasks.set(i, completed);
store.save(tasks);
sendJson(exchange, 200, completed);
return;
}
}
sendError(exchange, 404, "Task not found: " + id);
}

private void handleDeleteTask(HttpExchange exchange, int id)
throws IOException {
List<Task> tasks = store.load();
boolean removed = tasks.removeIf(t -> t.id() == id);
if (!removed) {
sendError(exchange, 404, "Task not found: " + id);
return;
}
store.save(tasks);
sendJson(exchange, 200, new MessageResponse("Deleted task " + id));
}

// ── Utilities ─────────────────────────────────────────────

private Task findTaskOrNull(int id) throws IOException {
for (Task task : store.load()) {
if (task.id() == id) return task;
}
return null;
}

private int parseIdFromPath(String path) {
String[] segments = path.split("/");
try {
return Integer.parseInt(segments[segments.length - 1]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Invalid task ID: " + segments[segments.length - 1]);
}
}

private String readRequestBody(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody()) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}

private void sendJson(HttpExchange exchange, int status, Object data)
throws IOException {
byte[] bytes = mapper.writeValueAsBytes(data);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(status, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}

private void sendError(HttpExchange exchange, int status, String message)
throws IOException {
sendJson(exchange, status, new ErrorResponse(message));
}

// ── Response records ──────────────────────────────────────

private record ErrorResponse(String error) {}
private record MessageResponse(String message) {}
private record HealthResponse(String status) {}
}

Gson vs Jackson comparison​

FeatureGsonJackson
Serializegson.toJson(object)mapper.writeValueAsString(object)
Deserializegson.fromJson(json, Type.class)mapper.readValue(json, Type.class)
Tree modelJsonObject / JsonElementJsonNode
Size~300 KB~1.7 MB (core + databind + annotations)
PerformanceGoodFaster for large payloads
EcosystemGoogleIndustry standard, Spring default
Record supportWorks out of the boxWorks out of the box (2.12+)

Both are excellent. Gson is simpler and smaller. Jackson is faster and more feature-rich. Choose based on your project's needs.

Gradle commands​

# Compile
./gradlew build

# Run the application directly
./gradlew run

# Create the fat JAR
./gradlew shadowJar

# Clean build output
./gradlew clean

# Clean + build
./gradlew clean build

# List all available tasks
./gradlew tasks

Running the app​

# Option 1: ./gradlew run (quick, for development)
./gradlew run

# Option 2: fat JAR (for deployment)
./gradlew shadowJar
java -jar build/libs/task-api-1.0.0-all.jar

Result:

Task API running on http://localhost:8080
Press Ctrl+C to stop.

Gradle wrapper​

Like Maven, Gradle has a wrapper so builds work without a global installation. Generate it:

gradle wrapper

This creates gradlew (Unix), gradlew.bat (Windows), and a gradle/wrapper/ directory. Commit these to version control.

Now anyone can build with:

./gradlew clean build
./gradlew shadowJar

The wrapper downloads the correct Gradle version automatically. Always use the wrapper in CI/CD and shared projects.

Build cache and incremental builds​

Gradle is often faster than Maven, especially on repeated builds, mainly because of:

  1. Incremental compilation - only recompiles changed files
  2. Build cache - reuses outputs from previous builds
  3. Gradle daemon - keeps a JVM running in the background

On a warm daemon with no changes, Gradle can often finish much faster because it reuses cached work. The exact speedup depends on the project and machine.

# First build
./gradlew build # ~5 seconds

# Second build (nothing changed)
./gradlew build # ~0.5 seconds (UP-TO-DATE)

Deploy with Gradle​

Update the deploy script from chapter 12:

#!/bin/bash
set -euo pipefail

SERVER="deploy@YOUR_SERVER_IP"
REMOTE_PATH="/opt/task-api"

echo "Building..."
./gradlew shadowJar -q

echo "Uploading..."
rsync -avz build/libs/task-api-1.0.0-all.jar "$SERVER:$REMOTE_PATH/task-api.jar"

echo "Restarting..."
ssh "$SERVER" "sudo systemctl restart task-api"

echo "Checking health..."
sleep 3
curl -sf https://yoursite.com/api/health && echo " - OK" || echo " - FAILED"

Which build tool should you use?​

SituationRecommendation
New project, small teamEither - personal preference
Joining an existing projectUse what the project uses
Enterprise / Spring BootMaven is more common
Android / KotlinGradle is required
Need maximum build speedGradle
Want simplest setupMaven

Both Maven and Gradle:

  • Download dependencies from Maven Central
  • Follow the same src/main/java directory layout
  • Produce the same JAR output
  • Support the same plugins and integrations
  • Work with all Java IDEs

The choice rarely matters for the end result. Pick one, learn it well, and switch later if needed.

Summary​

  • Gradle uses Kotlin DSL (build.gradle.kts) instead of XML - more concise and programmable.
  • implementation("group:artifact:version") declares dependencies (same Maven Central repository).
  • ./gradlew run starts the app directly; ./gradlew shadowJar creates a deployable JAR.
  • The Gradle wrapper (gradlew) ensures reproducible builds without a global installation.
  • Jackson (ObjectMapper) is the industry-standard JSON library - writeValueAsString and readValue.
  • Gradle's incremental builds and build cache make rebuilds significantly faster than Maven.
  • Neither tool is universally better - choose based on your team, project, and ecosystem.

For advanced Maven topics (BOMs, multi-module projects, profiles), see the Maven deep dive.

Next up: Streams & Lambdas - functional programming in Java with the Stream API.