Developing

June 25, 2026 ยท View on GitHub

Please also read CONTRIBUTING.md and CODE_OF_CONDUCT.md.

Tip

Contributor documentation lives in docs/. Start with docs/PROJECT_STRUCTURE.md for the module layout, and see docs/adr/ for Architecture Decision Records.

Important

Substantial changes must be accompanied by an Architecture Decision Record. See the criteria in CONTRIBUTING.md for when an ADR is required, and docs/adr/README.md for how to write one.

Prerequisites

  • JDK 25+ (Temurin distribution recommended)
  • Maven 3.9+
  • Docker or Podman (required for tests and dev mode)
  • A Java IDE (IntelliJ recommended)

Tip

We recommend sdkman for managing JDK and Maven installations, and mvnd for faster builds. The Makefile automatically uses mvnd when available, falling back to mvn.

Note

This guide uses make commands for brevity, and we recommend that you use make if you prefer CLI-centric workflows. If using make is not an option, you can inspect the full commands in Makefile and use them for your own custom workflows.

For IDE-centric workflows, we provide equivalent IntelliJ run configurations.

Core Technologies

TechnologyPurpose
Jakarta REST (JAX-RS)REST API specification
JerseyJAX-RS implementation
OpenAPIAPI specification
JDOPersistence specification
DataNucleusJDO implementation
JDBIDatabase access
FlywayDatabase migrations
MicroProfile ConfigConfiguration
JettyServlet container
PostgreSQLDatabase
TestcontainersIntegration testing
Protocol BuffersSerialization

Architecture Constraints

The following constraints apply project-wide. They exist to keep the codebase coherent as it evolves and to avoid steering changes in directions we are actively moving away from. For substantial changes, see also the Architecture Decision Record process in CONTRIBUTING.md.

REST API v1 is in maintenance mode

New endpoints must be added to API v2, which lives in the api module and follows a spec-first OpenAPI workflow. API v1 (in apiserver/src/main/java/org/dependencytrack/resources/v1) is code-first and uses Swagger annotations on JAX-RS resources. Touch v1 only when extending or fixing existing endpoints.

API v1 also reuses persistence models as REST DTOs. Do not propagate that pattern into v2. New endpoints must keep the API contract decoupled from the persistence layer.

Persistence: prefer JDBI and raw SQL

JDO and DataNucleus are being phased out. New persistence code should use JDBI with raw SQL. Avoid touching JDO entities unless the change genuinely requires it, and do not build new features on top of the JDO layer.

Throughput over latency

The system processes large volumes of components, vulnerabilities, and analyses. Optimize for throughput. Batch work, minimize network round trips, and avoid per-record hot paths that issue one query, request, or message at a time.

Strong consistency by default

Default to strong consistency. Eventual consistency is acceptable only when the use case explicitly demands it (typically for scale or availability reasons) and the trade-off is documented.

Simple and pragmatic over speculative future-proofing

Solve the problem in front of you. Avoid extra abstractions, configuration knobs, or extension points introduced for hypothetical future needs. It is cheaper to add an abstraction when a second concrete use case appears than to maintain one that has none.

Strong cohesion, loose coupling

Modules should be small and focused, with narrow, intentional interfaces between them. Reach across module boundaries through well-defined APIs rather than by importing internals. The ongoing modularization effort moves the codebase in this direction.

Building

Build the project:

make build

Tip

(Re-) building the entire project via make build is cheap due to build caching. You generally don't need to build modules selectively.

The resulting JAR is placed in ./apiserver/target as dependency-track-apiserver.jar. It ships with an embedded Jetty server, there's no need to deploy it in an application server like Tomcat or WildFly.

Build a container image:

make build-image

This produces the image ghcr.io/dependencytrack/apiserver:local.

Code Style

Java sources are checked with Spotless, which enforces the license header, removal of unused imports, and rejection of wildcard imports. The check runs as part of the build and in CI. Run it locally with:

make lint-java

Most findings can be fixed automatically using:

make format-java

Testing

Run all tests:

make test

Run a single test class:

make test-single MODULE=apiserver TEST=FooTest

Run multiple test classes:

make test-single MODULE=apiserver TEST="FooTest,BarTest"

Run a single test method:

make test-single MODULE=apiserver TEST="FooTest#testFoo"

Run e2e tests:

make test-e2e

Dev Mode

Dev mode launches the API server with auto-provisioned containers for PostgreSQL and the frontend. Containers are created on startup and, unless reuse is enabled (see Container Reuse), disposed of on shutdown.

make apiserver-dev

The API server will be available at http://localhost:8080. Frontend and PostgreSQL ports are logged during startup.

Dev mode specific configuration can be made in application-dev.properties.

Container Reuse

Dev mode is configured to reuse its containers across restarts, so PostgreSQL state (schema and data) is preserved and startup is faster. Reuse only takes effect once it has been opted into globally, by setting either testcontainers.reuse.enable=true in ~/.testcontainers.properties, or the TESTCONTAINERS_REUSE_ENABLE=true environment variable. Without it, containers are disposed on shutdown as usual. See the Testcontainers reuse docs.

To remove reused (or otherwise stale) dev services containers, e.g. to start from a clean slate, run:

make apiserver-dev-remove-containers

DataNucleus Bytecode Enhancement

Classes annotated with @PersistenceCapable must be enhanced post-compilation. Maven handles this automatically, but IDEs run their own builds and may skip the enhancement step.

If you see NucleusUserException: Found Meta-Data for class ... but this class is either not enhanced when running tests from your IDE, run:

make datanucleus-enhance

Then re-run the test. Ensure your IDE is not cleaning the target directory before execution.

Database Migrations

Schema changes are managed with Flyway. The API server owns the schema and applies pending migrations at startup.

Migrations live in migration/src/main/resources/org/dependencytrack/migration and follow Flyway's naming convention:

  • V<timestamp>__<description>.sql for versioned migrations, applied once in timestamp order. <timestamp> is YYYYMMDDHHMM (UTC).
  • R__<name>.sql for repeatable migrations (stored procedures, functions, views). Reapplied automatically when their content changes.

Adding a Migration

Scaffold a new versioned migration:

make new-migration NAME="add foo column to bar"

This creates an empty V<timestamp>__add_foo_column_to_bar.sql file. Add your DDL/DML to it.

For repeatable migrations, edit the relevant R__*.sql file directly, no new file needed.

Important

Do not modify versioned migrations already merged to main. Flyway rejects checksum mismatches on existing deployments. Add a new migration instead.

Note

Migrations run with outOfOrder=true so they can be backported to patch branches without blocking the next minor upgrade. See RELEASING.md.

Linting Migrations

New and modified migrations are linted with squawk to catch operationally unsafe DDL (missing CONCURRENTLY on indexes, blocking locks, NOT NULL columns without defaults, etc.) before they hit production deployments.

Run the linter locally:

make lint-migrations

The target only lints migrations changed relative to BASE_REF (default origin/main). For fork-based setups, point it at the upstream main branch:

make lint-migrations BASE_REF=upstream/main

The same check runs in CI on every pull request via the Lint Migrations job in ci-lint.yaml. PRs that introduce squawk findings will fail this job.

Suppress an individual finding only when justified, by annotating the SQL statement (see Disabling rules via comments):

-- squawk-ignore <rule-name>
ALTER TABLE ...;

Migrations and Transactions

Flyway wraps each migration script in a single transaction by default. A few Postgres DDL statements cannot run inside a transaction, most notably:

  • CREATE INDEX CONCURRENTLY
  • DROP INDEX CONCURRENTLY
  • REINDEX CONCURRENTLY
  • ALTER TYPE ... ADD VALUE

Running them in the default transactional mode will fail. Squawk may push you towards using these constructs, but it doesn't know about Flyway executing migrations in transactions implicitly.

Disable the transaction wrapper for that specific migration by adding a sidecar configuration file with the same name as the migration plus a .conf suffix, for example:

V202606151200__add_foo_bar_idx.sql
V202606151200__add_foo_bar_idx.sql.conf
# V202606151200__add_foo_bar_idx.sql.conf
executeInTransaction=false
-- V202606151200__add_foo_bar_idx.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS "FOO_BAR_IDX" ON "FOO" ("BAR");

See Flyway's script configuration docs for the full list of per-script overrides.

Warning

When executeInTransaction=false, the migration is no longer atomic. Keep these files small and idempotent (e.g. IF NOT EXISTS) so a partial failure can be safely re-run.

Build Cache

We use Maven build caching to speed up builds. If you encounter stale or unexplainable build issues, try clearing the cache and see if it resolves your issues:

make clean-build-cache