Developing
June 25, 2026 ยท View on GitHub
Please also read
CONTRIBUTING.mdandCODE_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
| Technology | Purpose |
|---|---|
| Jakarta REST (JAX-RS) | REST API specification |
| Jersey | JAX-RS implementation |
| OpenAPI | API specification |
| JDO | Persistence specification |
| DataNucleus | JDO implementation |
| JDBI | Database access |
| Flyway | Database migrations |
| MicroProfile Config | Configuration |
| Jetty | Servlet container |
| PostgreSQL | Database |
| Testcontainers | Integration testing |
| Protocol Buffers | Serialization |
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>.sqlfor versioned migrations, applied once in timestamp order.<timestamp>isYYYYMMDDHHMM(UTC).R__<name>.sqlfor 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 CONCURRENTLYDROP INDEX CONCURRENTLYREINDEX CONCURRENTLYALTER 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