Skip to content

PACE with Java (Spring Boot)

This tutorial walks through setting up PACE for a Java Spring Boot project. By the end, FORGE will write Java code, GATE will run your JUnit 5 test suite, and SENTINEL will review for Java-specific security issues.

Prerequisites

  • Java 17 or 21
  • Maven 3.9+ or Gradle 8+
  • An existing Spring Boot project with at least a minimal test suite
  • An API key for your LLM provider

Project layout assumed by this tutorial

my-service/
├── pace/ ← PACE subdirectory
│ └── pace.config.yaml
├── src/
│ ├── main/java/com/acme/
│ └── test/java/com/acme/
├── pom.xml ← or build.gradle
└── .pace/

1 — Install PACE

Terminal window
# From your repo root
git clone https://github.com/pace-framework-org/pace-framework-starter pace
cd pace
python -m venv .venv && source .venv/bin/activate
pip install PyYAML jsonschema anthropic

2 — Configure for Java

Edit pace/pace.config.yaml:

framework_version: "1.0"
product:
name: "Acme Service"
description: >
A Spring Boot REST API for the Acme platform. Serves mobile and web
clients. Handles orders, inventory, and payment processing.
github_org: "acme-corp"
sprint:
duration_days: 14
source:
dirs:
- name: "api"
path: "src/main/java/com/acme/"
language: "Java"
description: "Spring Boot application, domain models, and REST controllers"
- name: "tests"
path: "src/test/java/com/acme/"
language: "Java"
description: "JUnit 5 unit and integration tests"
tech:
primary_language: "Java 21"
ci_system: "GitHub Actions"
test_command: "mvn -q test" # or: ./gradlew test --quiet
build_command: "mvn -q compile" # or: ./gradlew compileJava
platform:
type: local # switch to github after verifying locally
llm:
provider: anthropic
model: claude-sonnet-4-6

Maven vs Gradle test commands

Build tooltest_commandbuild_command
Mavenmvn -q testmvn -q compile
Maven (skip integration tests)mvn -q test -Dexclude="**/*IT.java"mvn -q compile
Gradle./gradlew test --quiet./gradlew compileJava
Gradle (single module)./gradlew :api:test --quiet./gradlew :api:compileJava

3 — Set credentials

Terminal window
export ANTHROPIC_API_KEY="sk-ant-..."

4 — Write a Java-focused sprint plan

Create plan.yaml at your repo root:

release: v1.0
stories:
- id: story-1
title: "Product JPA entity and Spring Data repository"
status: pending
acceptance_criteria:
- "Product entity has id, name, price, and stockQuantity fields"
- "ProductRepository extends JpaRepository<Product, Long>"
- "ProductRepositoryTest covers findByName and save round-trip"
- "mvn test exits 0"
out_of_scope:
- "REST endpoints"
- "Category associations"
- id: story-2
title: "ProductCatalogService with validation"
status: pending
acceptance_criteria:
- "createProduct() throws IllegalArgumentException for negative price"
- "updateStock() throws InsufficientStockException when quantity < 0"
- "Service layer is unit-tested with Mockito (no database)"
- "mvn test exits 0"
- id: story-3
title: "ProductController with GET/POST/PUT endpoints"
status: pending
acceptance_criteria:
- "GET /products returns 200 with list"
- "POST /products returns 201 with Location header"
- "PUT /products/{id} returns 200 or 404"
- "Integration tests use @SpringBootTest and MockMvc"
- "mvn test exits 0"

5 — Run Day 1

Terminal window
cd pace
python pace/orchestrator.py --day 1

What FORGE does with Java

FORGE’s tool-calling loop will:

  1. Read existing source files under src/main/java/com/acme/ and src/test/java/com/acme/
  2. Understand the package structure and Spring conventions
  3. Write new .java files using JPA annotations, Spring Data interfaces, and JUnit 5 patterns
  4. Run mvn -q compile (build_command) to catch syntax errors before running tests
  5. Run mvn -q test and read failures to self-correct

A typical FORGE run for Day 1:

[FORGE] Reading src/main/java/com/acme/AcmeServiceApplication.java ...
[FORGE] Reading pom.xml ...
[FORGE] Writing src/main/java/com/acme/catalog/domain/Product.java ...
[FORGE] Writing src/main/java/com/acme/catalog/repository/ProductRepository.java ...
[FORGE] Writing src/test/java/com/acme/catalog/repository/ProductRepositoryTest.java ...
[FORGE] Running build: mvn -q compile ...
[FORGE] Running tests: mvn -q test ...
[FORGE] All tests pass. Calling complete_handoff.

What SENTINEL checks for Java

SENTINEL applies Java-specific checks including:

  • Injection risks: SQL injection via native queries, SpEL injection in @Query
  • Deserialization: unsafe use of ObjectInputStream, XmlDecoder
  • Secrets in source: hardcoded passwords in application.properties or @Value
  • Dependency vulnerabilities: flags commonly vulnerable versions (Log4Shell, Spring4Shell patterns)
  • JWT handling: weak signing algorithms, missing expiry validation

6 — Handling Maven multi-module projects

For multi-module projects, point PACE at the specific module you are sprinting on:

source:
dirs:
- name: "catalog-service"
path: "catalog-service/src/main/java/"
language: "Java"
description: "Catalog bounded context — products, categories, pricing"
tech:
test_command: "mvn -q test -pl catalog-service"
build_command: "mvn -q compile -pl catalog-service -am"

7 — Example: switching to GitHub platform

Once your local runs succeed, switch to GitHub to get CI polling and automatic PRs:

platform:
ci: github
Terminal window
export GITHUB_TOKEN="ghp_..."
export GITHUB_REPOSITORY="acme-corp/acme-service"
pip install PyGithub

See Switch Platform for the full credential reference.

8 — FORGE hints for Java

The most effective way to improve FORGE’s output is to describe your team’s conventions in product.description. FORGE reads this before writing any file.

Useful hints for Spring Boot:

product:
description: >
A Spring Boot REST API. Use constructor injection everywhere — never @Autowired
field injection. All DTOs are Java records. Domain entities use @Entity with
JPA annotations. Business logic in @Service classes; controllers call services,
services call repositories. Use @ExtendWith(MockitoExtension.class) with @Mock
and @InjectMocks for unit tests. Follow Google Java Style Guide, 120 char limit.
All new code must pass Checkstyle before tests run.

9 — Test coverage (JaCoCo)

Add JaCoCo to pom.xml:

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>check</id>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>

Switch test_command to mvn -q verify so JaCoCo’s check goal runs after tests:

tech:
test_command: "mvn -q verify"

For Gradle, add the JaCoCo plugin and jacocoTestCoverageVerification task.


10 — Linter and formatter (Spotless)

Use Spotless for consistent formatting and Checkstyle for style rules:

<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.43.0</version>
<configuration>
<java>
<googleJavaFormat/>
</java>
</configuration>
</plugin>

Set build_command to run the linter before tests:

tech:
build_command: "mvn -q spotless:check"
test_command: "mvn -q verify"

Include the expectation in product.description so FORGE generates clean code from the start: "All code must pass Spotless with Google Java Format style."


11 — Database testing

H2 in-memory (fast unit tests)

Add H2 to test scope in pom.xml:

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

Use @DataJpaTest for repository tests — Spring Boot auto-configures H2:

@DataJpaTest
class ProductRepositoryTest {
@Autowired ProductRepository repository;
@Test
void saveAndRetrieve() {
var saved = repository.save(new Product("Widget", 9.99));
assertThat(repository.findById(saved.getId())).isPresent();
}
}

Tell FORGE about the pattern:

product:
description: >
Repository tests use @DataJpaTest with H2. Never use the production
PostgreSQL URL in tests. Service tests use @ExtendWith(MockitoExtension.class)
with mocked repositories — no Spring context.

Testcontainers (real PostgreSQL)

For integration tests that need real PostgreSQL behaviour:

@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
}

Mark with @Tag("integration") and exclude from the default test command:

tech:
test_command: "mvn -q test -Dexclude='**/*IT.java,**/*IntegrationTest.java'"

12 — Mock patterns (Mockito)

@ExtendWith(MockitoExtension.class)
class ProductCatalogServiceTest {
@Mock ProductRepository repository;
@InjectMocks ProductCatalogService service;
@Test
void throwsForNegativePrice() {
assertThrows(IllegalArgumentException.class,
() -> service.createProduct("Widget", -1.0));
verifyNoInteractions(repository);
}
}

FORGE hint: "Use @ExtendWith(MockitoExtension.class), not @RunWith(MockitoJUnitRunner.class). Always use @InjectMocks for the class under test."


13 — Multi-day sprint (Day 2 builds on Day 1)

After Day 1 ships, SCRIBE records the new classes in .pace/context/engineering.md. Day 2’s FORGE reads this before writing:

[FORGE] Reading .pace/context/engineering.md ...
[FORGE] Found: com.acme.catalog.domain.Product (JPA entity, ProductRepository extends JpaRepository)
[FORGE] Writing com.acme.catalog.service.ProductCatalogService ...
[FORGE] Writing src/test/java/com/acme/catalog/service/ProductCatalogServiceTest.java ...

Story-2 acceptance criteria can safely reference Product and ProductRepository by name — FORGE already knows where they live.


14 — Advisory clearance story

After several sprint days, add a clearance story:

- id: story-5
title: "Security and DevOps advisory clearance"
status: pending
acceptance_criteria:
- "All open SENTINEL advisories resolved or escalated with written justification"
- "All open CONDUIT advisories resolved or escalated"
- "mvn verify exits 0 after any remediation changes"

Common Java SENTINEL advisories that accumulate:

  • @Query with nativeQuery = true and string concatenation — fix: use parameterised ?1 placeholders
  • ObjectInputStream.readObject() without class filtering — fix: use ValidatingObjectInputStream
  • Hardcoded passwords in application.properties — fix: use ${ENV_VAR} placeholders

15 — CI build caching

Cache Maven’s local repository to avoid re-downloading dependencies on every PACE run:

- uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-

For Gradle:

- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

16 — Windows notes

On Windows, use the Maven wrapper (mvnw.cmd) or Gradle wrapper (gradlew.bat) instead of the bare command:

tech:
test_command: "mvnw.cmd -q test"
build_command: "mvnw.cmd -q spotless:check"

Java and the JDK toolchain work natively on Windows — no WSL required.


Common issues

mvn: command not found PACE runs test_command from the repo root. If Maven is not on PATH inside the pace/ virtualenv, use the Maven wrapper: test_command: "../mvnw -q test" (adjust path relative to repo root).

Gradle daemon conflicts If FORGE runs the Gradle daemon and your shell has a separate daemon running, use --no-daemon: test_command: "./gradlew test --quiet --no-daemon".

Test output too verbose for GATE GATE reads the full test output. With Surefire, add -Dsurefire.failIfNoSpecifiedTests=false and ensure <reportFormat>plain</reportFormat> in your POM for clean failure messages.

FORGE writes to wrong package If your base package is not com.acme, update source.dirs[].path and source.dirs[].description to reflect your actual package structure so SCRIBE and FORGE understand the layout.