Share
## https://sploitus.com/exploit?id=15E26C5F-D228-530C-8A7A-1886D6A922BF
# dependo

A proof of concept for state-of-the-art observability in Spring Boot 4. Five services emit
metrics, traces and logs through OpenTelemetry, and the point of the whole exercise is that
you can move freely between those three signals: from a metric to the exact request behind
it through an exemplar, from that request's trace to its logs, and from any log line back to
the trace it came from.

The product is deliberately thin. You submit a Maven `pom.xml` and you get a report telling
you, for each dependency, which version resolved, what the latest stable version is, how far
behind that leaves you, and whether anything published against it is a known vulnerability.
That is roughly seventy-five lines of real business logic in total. The interactions between
the services and their stores are the curriculum here, not the domain.

## The pipeline

```
  client
    โ”‚  POST /poms   (multipart, field name "pom")
    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   pomSubmitted   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  dependo-service โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚ resolver-service โ”‚
โ”‚    submission    โ”‚                  โ”‚  parse the POM   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
   MinIO, Postgres                             โ”‚
                                               โ”‚  pomResolved
                                               โ”‚  one routing key, two queues bound
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ–ผ                                 โ–ผ
                   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                   โ”‚ freshness-service โ”‚        โ”‚ vulnerability-service โ”‚
                   โ”‚  latest versions  โ”‚        โ”‚   is it vulnerable?   โ”‚
                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    repo1.maven.org                          โ”‚
                              โ”‚                              โ”‚ POST /advisories/queries
                              โ”‚                              โ–ผ
                              โ”‚                   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚                   โ”‚  advisory-service โ”‚
                              โ”‚                   โ”‚  read-through OSV โ”‚
                              โ”‚                   โ”‚      mirror       โ”‚
                              โ”‚                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚                    OpenSearch, api.osv.dev
                              โ”‚                              โ”‚
                              โ”‚ freshnessCompleted           โ”‚ vulnerabilityCompleted
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ–ผ
                                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                    โ”‚   dependo-service   โ”‚
                                    โ”‚ fan-in and assemble โ”‚
                                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                    DynamoDB join, Postgres
                                               โ”‚
                                               โ–ผ
                                GET /reports/{artifactId}  โ†’  200
```

dependo-service takes the submission, parks the POM in MinIO, opens a report row in Postgres
and publishes `pomSubmitted`. resolver-service parses the POM and publishes `pomResolved`.
That one routing key has two queues bound to it, so the broker fans the work out to both
branches in parallel. freshness-service asks repo1 for the latest version of each dependency
and classifies how far behind you are. vulnerability-service makes a single batch call to
advisory-service, which keeps a read-through mirror of OSV advisories in OpenSearch and only
goes out to the real OSV API when its local copy has gone stale. Both branches publish their
results back to dependo-service, which joins them in DynamoDB, elects exactly one assembler
and writes the finished report to Postgres.

Everything above travels as events over a single RabbitMQ topic exchange named `dependo`.
The one exception is worth remembering: the only service-to-service REST call in the whole
system is vulnerability-service calling advisory-service.

| Service | Package | Port | What it does |
|---|---|---|---|
| dependo-service | `dev.dependo.service` | 8080 | The boundary API, and the owner of the fan-in and report assembly. |
| resolver-service | `dev.dependo.resolver` | 8081 | Parses the POM, filters scopes and interpolates `${property}` versions. |
| freshness-service | `dev.dependo.freshness` | 8082 | Looks up latest versions in repo1 and classifies the distance behind. |
| vulnerability-service | `dev.dependo.vulnerability` | 8083 | Asks advisory-service about the whole dependency list in one call. |
| advisory-service | `dev.dependo.advisory` | 8084 | A read-through OSV mirror backed by OpenSearch. The only service with no messaging. |

## Prerequisites

You need a JDK and Docker, and nothing else. The build targets Java release 25, so a JDK of
25 or newer will work โ€” there is no `.sdkmanrc` or `.tool-versions` in the repository, so do
not go looking for one. Maven itself does not need to be installed, because `./mvnw`
bootstraps Apache Maven 3.9.16 for you.

Docker is required with Compose v2, and not only for running the system. Every service's
integration tests use Testcontainers, so `./mvnw verify` needs a working Docker daemon too.
Give Docker some real memory: eleven containers run when you bring the applications up
alongside the infrastructure, and OpenSearch alone is pinned to a 512 MB heap.

The runbooks in this repository use `curl` and `jq` throughout. Internet access matters only
for the smoke tier, which deliberately calls the real repo1 and OSV rather than stubbing
them.

## Running it

There are two ways to run dependo, and the difference between them is the single most common
source of confusion, so it is worth getting straight before you start.

### The IDE loop

This is the better loop while you are developing. Start the backing containers and then run
whichever service you are working on from your IDE:

```bash
docker compose up -d
```

You do not strictly need even that first command. Every service declares
`spring-boot-docker-compose` and points at `../compose.yaml` with
`lifecycle-management: start-only`, so starting any single service brings the infrastructure
up by itself and leaves it running when that service stops. Because the path to the compose
file is relative, the working directory has to be the module directory, which is what an IDE
does by default.

In this loop the services listen on **8080 through 8084**.

### The full-compose loop

Here everything runs in containers, including the applications. Build the images first,
which is opt-in because buildpacks take a few minutes, and then bring the stack up with the
`apps` profile:

```bash
./mvnw -Pimages -DskipTests package
docker compose --profile apps up -d
```

In this loop the services listen on **18080 through 18084**.

### The port rule

Application ports are the same numbers plus ten thousand under `--profile apps`. That is
deliberate, so that a service running from your IDE and the same service running in compose
can never collide. It does mean that every `curl` command you read in this repository has to
be matched to the loop you are actually in: the boundary API is at
 from the IDE and  under compose.

## Check that it works

Submit one of the sample projects. The command below assumes the full-compose loop, so
substitute 8080 for 18080 if you are running from your IDE.

```bash
curl -i -F "pom=@samples/sample-app-legacy/pom.xml" http://localhost:18080/poms
```

You get a `202` back, with a `Location` header pointing at the report and an `X-Trace-Id`
header carrying the trace of this pipeline run. Keep that trace id โ€” it is the entry point
from your terminal into Grafana.

Now poll for the report. A `404` means the pipeline is still working, which is the deliberate
contract rather than an error:

```bash
curl -s http://localhost:18080/reports/sample-app-legacy | jq .
```

Once it turns `200` you will see each dependency with its resolved version, its latest
version, how far behind it is, and its advisories. The body carries the same `traceId` that
came back in the header, so every report links to its own trace.

Finally, open Grafana at  and paste that trace id into Explore. There
is nothing to configure: the `grafana/otel-lgtm` image ships Tempo, Loki and Prometheus
already wired to each other, so the trace, its logs and its metrics are all reachable from
one another.

For the full tour of every navigation direction, follow [demo.md](demo.md). It takes about
ten minutes and it is the real proof of what this POC is for.

## Repository map

| Path | What it is |
|---|---|
| `dependo-common` | Shared library: the event types, the messaging topology, the telemetry auto-configuration, the instrumented AWS clients and the DynamoDB lookup cache. Not a service. |
| `dependo-service` | The boundary API and the fan-in owner. |
| `resolver-service` | POM parsing and version resolution. |
| `freshness-service` | Latest-version lookups against repo1. |
| `vulnerability-service` | The batch advisory query. The thinnest service in the system. |
| `advisory-service` | The read-through OSV mirror over OpenSearch. |
| `e2e-smoke` | A test-only module. Deliberately excluded from `./mvnw verify`. |
| `samples/` | Hand-written input POMs, including the two deliberately broken ones. |
| `kanban/` | The board and the story files. See [process.md](process.md). |

One naming detail will trip you up if nobody warns you. The directory `dependo-service` maps
to the package `dev.dependo.service`, but the other four services drop the `-service` suffix
in their package names, so `resolver-service` lives in `dev.dependo.resolver`.

## Building and testing

```bash
./mvnw verify                            # the full build and test suite
./mvnw -Pimages -DskipTests package      # the five container images, via buildpacks
docker compose up -d                     # infrastructure only, for the IDE loop
docker compose --profile apps up -d      # infrastructure plus the five services
./mvnw -Psmoke -pl e2e-smoke test        # the smoke tier, against a running stack
```

There are three tiers of test, and the distinction between them is about what they are
allowed to touch. Unit tests are plain and fast. Integration tests use Testcontainers for
real brokers and stores, but they never reach the internet. The smoke tier is the only one
that does, which is exactly why it is gated behind the `smoke` profile and skipped by
default. Run it against a stack that is already up; it defaults to
 and you can point it elsewhere with `-Ddependo.base-url`.

## How the telemetry is wired

This section is orientation only. [telemetry-research.md](telemetry-research.md) is the real
reference, and it is worth reading before you touch any telemetry dependency or setting.

The integration is Boot-native, through `spring-boot-starter-opentelemetry`, rather than the
OpenTelemetry Java agent. [otel-agent-vs-boot-native.md](otel-agent-vs-boot-native.md) is the
decision record and it argues both sides honestly. The short version is that testability
decided it: under the agent, telemetry does not exist below the end-to-end tier, which is
incompatible with a project that tests everything. The two approaches must never be combined
in one JVM, because you get every signal twice.

No OTLP endpoint is configured anywhere in the application sources, and that is the design
rather than an oversight. In the IDE loop, `spring-boot-docker-compose` recognises the
`grafana/otel-lgtm` container and injects all three endpoints for you. In the compose loop,
the containers are told where the backend is through the standard `OTEL_*` environment
variables, which Boot 4.1 maps onto the same properties.

Logs are the one signal that needs a manual step, because Boot auto-configures OTLP log
export but not the bridge from Logback into the SDK. `dependo-common` supplies the appender
and installs it, so no individual service has to think about it.

Three settings explain most of what you will see in Grafana. The trace sampling probability
is 1.0, and if you lower it most traces and their exemplars quietly disappear.
`spring.application.group` is set to `dependo`, which becomes the `service.namespace`
attribute on every signal, and that is why every Loki query in the runbooks begins with
`{service_namespace="dependo"}`. And `project.artifactId` travels as OpenTelemetry baggage
into every service's logging context, which is what lets you ask "show me everything that
happened to project X" without having a trace id at all.

One version constraint is worth stating here because it is the thing most likely to be broken
by a well-meaning upgrade. The OpenTelemetry instrumentation BOM stays at `2.28.1-alpha`,
because it has to match the OpenTelemetry API 1.62.0 that Spring Boot 4.1.0 manages. A
mismatch does not fail the build; it fails at runtime with a `NoSuchMethodError`.

## Ports, URLs and credentials

Everything here is local and disposable, so the credentials are dev-grade on purpose.

| What | Where | Credentials |
|---|---|---|
| Grafana |  | anonymous access, as Admin |
| OTLP ingest | 4317 for gRPC, 4318 for HTTP | โ€” |
| RabbitMQ management |  | `dependo` / `dependo` |
| MinIO console |  | `dependo` / `dependo-secret` |
| MinIO S3 API |  | as above |
| Postgres | 5432 | `dependo` / `dependo`, database `dependo` |
| DynamoDB Local | 8000 | โ€” |
| OpenSearch |  | none; the security plugin is disabled |
| The five services | 8080โ€“8084 from the IDE, 18080โ€“18084 under compose | โ€” |

Prometheus, Tempo and Loki are not published on the host. You reach them through Grafana,
which is how you normally want to use them anyway. If you do need Prometheus directly it
lives on port 9090 inside the lgtm container, which is why [demo.md](demo.md) queries it
through `docker compose exec`.

## When something looks wrong

- **You are calling the wrong port.** The boundary API is on 18080 under `--profile apps` and
  on 8080 from the IDE. This is the single most common mistake.
- **Spans stop arriving after an OTLP change.** In the compose environment the endpoints must
  be the HTTP ones on 4318. Pointing traces at the gRPC port 4317 makes every span export
  fail silently, with nothing in the logs to tell you.
- **Traces or exemplars have gone missing.** Check that the sampling probability is still 1.0.
  Lowering it loses traces and their exemplars without any error.
- **A service will not start the infrastructure.** `spring.docker.compose.file` is the
  relative path `../compose.yaml`, so the working directory has to be the module directory.
- **You are looking for `/actuator/**`.** There is none. No Actuator starter is declared in
  any module and no management endpoints are configured.
- **`./mvnw verify` fails oddly.** It needs a running Docker daemon, because the integration
  tests use Testcontainers.

## Where to go next

| Document | What it is for |
|---|---|
| [demo.md](demo.md) | The cross-signal walkthrough. Follow it once, on a live system, and you will have exercised every navigation direction. Start here. |
| [drills.md](drills.md) | Three deliberate failures and the different shapes they leave in the telemetry. A handled rejection, an outage, and a branch that dies silently. |
| [requirements.md](requirements.md) | The canonical specification. Fifteen sections covering the API, the report model, the services, messaging, storage, telemetry design, testing and the decision log. |
| [telemetry-research.md](telemetry-research.md) | Verified Spring Boot 4 and OpenTelemetry know-how: exact versions, wiring recipes and a pitfall checklist. Read it before touching telemetry. |
| [otel-agent-vs-boot-native.md](otel-agent-vs-boot-native.md) | Why this project uses the Boot-native integration instead of the Java agent, including when the agent would be the better answer. |
| [process.md](process.md) | How work happens here: the file-based Kanban, the Definition of Done and the git rules. |
| [kanban/board.md](kanban/board.md) | The board itself, and the single source of truth for story status. |

Development runs as a file-based Kanban described in `process.md`, with one git commit per
story. All nineteen stories are currently Done and the Backlog is empty.

## License

dependo is licensed under the Apache License 2.0. The full text is in [LICENSE](LICENSE).