Prerequisites & the Toolchain

SDKMAN, JDK 25, Maven, the Quarkus and Camel CLIs, Docker Engine and the compose observability stack, and a verify-your-setup gate.

Chapter 1Setting Up

The introduction promised a monolith you can actually run, extractions you can actually watch happen, and a behavior-equivalence suite you can actually fail and fix. None of that is a thought experiment past this point — starting with the next chapter, every claim in this book is backed by code sitting in this repository, and this chapter’s entire job is to get that code building and talking to itself on your machine before a single pattern gets discussed. There is no new example directory here to point at; instead, this chapter walks you through the toolchain and then through the three pieces of code that already exist — examples/00-monolith/, examples/01-strangler-proxy/, and examples/02-review-service/ — far enough to prove they run. Chapters 3, 14, and 15 are where each one gets explained properly; this chapter’s bar is lower and more mechanical: clone, install, start, and confirm.

The local toolchain split into required (JDK 25, Maven) and optional (Quarkus CLI, Camel CLI) pieces, the docker compose stack on Docker Engine (Postgres, Kafka in KRaft mode, the Grafana LGTM bundle), and the examples/, tooling/, demos/, infra/, scripts/, and _docs/ directories those two feed.
Figure 1.1 — The local toolchain and the repository layout it builds, runs, and stands up.

What you need, and where it comes from

Seven tools make an appearance across this book, and only two of them are required for everything that follows. The rest are scoped to specific parts, and it is worth knowing which is which before you install anything, because installing tools you will not touch until Part 9 is wasted effort today.

A Java Development Kit, version 25. Every JVM example in this book — the Spring Boot monolith, the Quarkus services, the Camel routes — targets JDK 25, and the monolith’s pom.xml pins both <java.version> and <maven.compiler.release> to 25 explicitly rather than leaving them to whatever the build machine happens to have on its PATH. That pin is deliberate: a modernization book that claims a service “starts in 0.048 seconds” (you will see that exact number again in Chapter 7) needs readers measuring against the same baseline the book measured against, not a JDK two major versions behind it. The recommended way to get JDK 25 is SDKMAN, a shell-based version manager for the JVM ecosystem that lets you install and switch between JDK distributions without touching your system package manager:

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 25-tem
sdk use java 25-tem
java -version

The -tem suffix selects the Eclipse Temurin distribution, the same one the project’s GitHub Actions CI job installs (actions/setup-java@v4 with distribution: temurin, java-version: "25") — matching your local JDK vendor to CI’s is one fewer variable the next time a build behaves differently in the two places. If your Fedora or RHEL host already has a JDK 25 package you are comfortable with, that works too; SDKMAN is the recommendation because it makes it trivial to hold two or three JDKs side by side and switch per-shell, which matters once you are flipping between this book’s JVM-mode and native-image examples in Part 5.

Apache Maven 3.9.x, or just the Maven Wrapper the individual projects already carry. This is the one place in the toolchain where the two examples in this repository differ from each other, and the difference is worth internalizing now rather than discovering it as a build failure later. examples/00-monolith/ — the Spring Boot “before” picture — has no mvnw script committed; it expects a Maven installation already on your PATH, invoked as plain mvn. examples/01-strangler-proxy/ and examples/02-review-service/ — the Quarkus and Camel-on-Quarkus “after” pieces — each carry their own mvnw / mvnw.cmd wrapper and a .mvn/wrapper/ directory, invoked as ./mvnw from inside that project’s own directory. The wrapper exists precisely so a Quarkus project’s exact Maven version travels with the repository instead of depending on whatever happens to be installed globally; the monolith, as the fixed “before” reference the whole book measures every extraction against, is simpler and just needs a real Maven on the path. If you do not already have one, SDKMAN installs Maven the same way it installs the JDK:

sdk install maven 3.9.9
mvn -version

The Quarkus CLI — optional, but convenient. Nothing in this book requires the standalone quarkus command; every Quarkus project here ships its own wrapper and builds fine with ./mvnw package or ./mvnw quarkus:dev alone. The CLI is worth having anyway once you reach Part 5’s extraction chapters, because quarkus dev, quarkus info, and quarkus ext list are materially faster to type and easier to script than their Maven-goal equivalents, and a few later “ADLC in Action” callouts assume it is on your PATH when they narrate an agent driving a running dev-mode instance through the Quarkus Agent MCP server. Install it via SDKMAN alongside the JDK and Maven:

sdk install quarkus
quarkus --version

The Camel CLI and Camel TUI — optional, scoped to Part 5 onward. You will not need either to build or run examples/01-strangler-proxy/ in this chapter — it is an ordinary Quarkus Maven module and ./mvnw package is all it takes. The CLI becomes useful later, when Chapter 14 and beyond have you prototyping a new Camel route interactively (camel init, camel run) before it graduates into a committed Maven project, and the Camel MCP server this book’s agent loop leans on for route validation and EIP-catalog lookups assumes the same JBang-based tooling underneath. If you want it installed now rather than when Part 5 asks for it:

sdk install camel
camel --version

Docker Engine and the docker compose plugin. The local stack (Postgres, Kafka, the Grafana LGTM observability bundle), Testcontainers and Quarkus Dev Services in the test suites, the native-image container builds, and the minikube cluster in Part 9 all run on Docker Engine — the docker-ce packages from Docker’s own repository, talking to /var/run/docker.sock through the docker context named default. Docker Desktop is never required. Compose is the v2 CLI plugin, invoked as docker compose (a space, not the hyphenated legacy docker-compose binary). One container engine for every path is the point: the image tags .env pins for the compose stack are the same ones the test containers and the cluster pull, so the everyday dev loop and the containers a test run spins up for itself cannot drift apart. The book is written and verified on Fedora and RHEL hosts (bare metal or VM). Install from Docker’s repository — Fedora (dnf5) is shown; on RHEL the repository line is sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo:

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"        # then log out and back in

Remove podman-docker first if it is installed — it shadows the docker command. Then confirm the engine, the context, and the plugin:

docker context show        # default
docker version --format '{{.Server.Version}}'
docker compose version

With the engine on the default socket, Testcontainers and Quarkus Dev Services find it with no further configuration (no DOCKER_HOST export needed).

Node.js and Newman — for the behavior-equivalence suite. The suite that gates every extraction in this book from Chapter 15 onward is a Postman collection run through Newman, Postman’s command-line collection runner, which itself runs on Node.js. You do not need Node for anything else in this book — no JavaScript application code appears anywhere in the examples — so a recent LTS release (Node 22 or newer; the project’s CI job uses Node 22 via actions/setup-node@v4) is sufficient — sudo dnf install -y nodejs npm on Fedora and RHEL. Install Newman globally once Node is in place:

npm install -g newman
newman --version

Git, for cloning the repository and following along with the commit history this book cites directly — Chapter 7, for instance, walks through ten real commit hashes from this project’s own history as its evidence trail, and you will want git log working locally to read them in context rather than taking the book’s word for it.

Ruby and Bundler — only if you intend to build this Jekyll site locally. Reading the chapters does not require Ruby at all; this book is published as a static GitHub Pages site and a browser is the only client most readers need. If you want to preview chapter edits or regenerate diagrams on your own machine, the root Gemfile pins Jekyll to the same gem versions GitHub Pages runs in production (gem "jekyll", "~> 4.3", plus the jekyll-feed, jekyll-sitemap, and jekyll-seo-tag plugins GitHub Pages provisions automatically), installed the ordinary Bundler way:

gem install bundler
bundle install
bundle exec jekyll serve

Clone the repository and configure the stack

With the toolchain in place, start by cloning the repository and copying the tracked environment template to a local, gitignored .env file. This two-step pattern — a tracked .env.example with safe, documented defaults, and a gitignored .env you actually run against — is how the project keeps image tags and credentials both reproducible and out of version control:

git clone https://github.com/patterncatalyst/modernizing-enterprise-applications.git
cd modernizing-enterprise-applications
cp .env.example .env

Open .env and skim it before moving on; it is short and every line is commented. It pins the image tags — the Grafana otel-lgtm bundle (0.36.0), Postgres (18.6-alpine), Apache Kafka (4.3.1), Debezium Connect (3.7.0.Final) and Apicurio Registry (3.3.3) — and the host ports, along with the monolith’s database name, user, and password, and a fixed Kafka KRaft cluster identity. None of these values are secret (the password is literally monolith_dev_only), but pinning them in one tracked file rather than hard-coding them into compose.yaml itself is what lets a later chapter bump an image tag in one place and have every script, every CI job, and every service’s own configuration pick the change up without a search-and-replace across the repository.

Bring up the infrastructure stack

The project’s local infrastructure — Postgres (the monolith’s and, for now, Review’s shared database), single-broker KRaft-mode Kafka (no ZooKeeper, needed from the event-driven extractions in Part 6 onward), and the Grafana LGTM observability bundle (Loki, Grafana, Tempo, Mimir, and an embedded OpenTelemetry Collector) — is defined once in the root compose.yaml and brought up with a single helper script:

scripts/stack-up.sh

Read what that script actually does rather than treating it as a black box, because the mechanics matter the first time something does not come up cleanly. It resolves its own location so it works regardless of your current directory, copies .env.example to .env for you if you skipped the step above, checks that docker and the docker compose plugin are present, then runs docker compose --env-file .env up -d and polls docker compose ps in a loop until every service’s healthcheck reports healthy (or sixty retries elapse, in which case it prints whatever state it last saw and gets out of your way rather than hanging forever). Each of the three core services carries its own healthcheck in compose.yaml: Postgres via pg_isready -h 127.0.0.1 (over TCP, because the image’s first-boot init server answers on the Unix socket before the real server is listening), Kafka via kafka-broker-api-versions.sh against its own broker, and the LGTM bundle via a plain curl against Grafana’s /api/health endpoint, because the upstream otel-lgtm image does not ship wget, and a healthcheck template that assumes it will silently fail with exit code 127 forever. When the script finishes, it prints the addresses you’ll use for the rest of this chapter and every chapter after it:

Grafana:    http://localhost:3000
Prometheus: http://localhost:19090 (host 19090; Cockpit owns 9090)
Postgres:   localhost:5432 (see .env for db/user/password)
Kafka:      localhost:9092 (host) / kafka:9094 (compose network)

Open http://localhost:3000 in a browser once the script reports healthy. Grafana is configured for anonymous admin access in this local stack (GF_AUTH_ANONYMOUS_ENABLED=true), which is appropriate for a disposable dev environment and would be the wrong default anywhere else — you will not see any dashboards populated yet, because nothing has emitted a trace, metric, or log to it; that wiring is the subject of Part 9. For now, a reachable Grafana login screen with no authentication prompt is simply your confirmation that the stack is actually up. When you are done for the session, tear it down with the matching script, which by default preserves the named Postgres and Kafka volumes so your seed data and topics survive a restart:

scripts/stack-down.sh        # stop containers, keep volumes
scripts/stack-down.sh -v     # stop containers, wipe volumes (fresh state)

Build and run the monolith

With Postgres up, build the Spring Boot monolith with plain Maven — no wrapper, as noted above — from the repository root:

mvn -f examples/00-monolith package

This compiles the six bounded contexts (order, inventory, payment, shipping, notification, review) into a single monolith.jar under examples/00-monolith/target/. Before you run it, there is one fragile detail worth documenting rather than hitting as a confusing connection failure: examples/00-monolith/src/main/resources/application.yml ships a default datasource password of monolith for convenience, but the actual Postgres container the stack just started was provisioned from .env’s POSTGRES_PASSWORD=monolith_dev_only. Spring Boot’s standard environment-variable override (SPRING_DATASOURCE_PASSWORD) is how the application’s committed defaults and the stack’s actual credentials get reconciled at runtime, without either file needing to hard-code the other’s value:

cd examples/00-monolith
SPRING_DATASOURCE_PASSWORD=monolith_dev_only \
    java -jar target/monolith.jar

On startup, Flyway runs the monolith’s own migrations (V1__init_schema.sql, V2__seed_data.sql) against the monolith database automatically — there is no separate migration step to remember — and the application binds to port 8080. A GET http://localhost:8080/api/reviews?sku=SKU-WIDGET-001 against a freshly seeded database should return a non-empty JSON array; if it does, the monolith is up and seeded correctly.

Build and run the Quarkus Review service and the Camel strangler proxy

The two “after” pieces this chapter asks you to stand up both use the Maven Wrapper, invoked from inside their own project directories, which is the pattern you will repeat for every Quarkus and Camel project in the rest of this book:

cd examples/02-review-service
./mvnw package
java -jar target/quarkus-app/quarkus-run.jar

The Review service listens on port 8081 and, in its current form, reads and writes the same reviews table in the same Postgres database the monolith owns — true per-service data ownership is a Part 6 concern, not this chapter’s or even Part 5’s — so there is nothing extra to migrate or seed here; the monolith’s Flyway run already created the table this service needs. In a second shell, build and run the Camel strangler proxy the same way:

cd examples/01-strangler-proxy
./mvnw package
java -jar target/quarkus-app/quarkus-run.jar

The proxy listens on port 8888 and reverse-proxies every request under /api/** to one of the two backends above, chosen by the strangler.review.enabled property: /api/reviews/** goes to the Review service on 8081 when the flag is on (the committed default, as of this project’s own r02 cutover) and everything else always goes to the monolith on

  1. If you would rather explore either Quarkus project with live reload instead of a packaged jar while you are getting oriented, ./mvnw quarkus:dev from inside either directory starts it in dev mode and recompiles changes on the fly — a detail you will use constantly from Part 5 onward and might as well try once now.

Run the behavior-equivalence suite

With all three services reachable — monolith on 8080, Review on 8081, proxy on 8888 — you are ready to run the same check every extraction chapter in this book runs before it trusts a change: the behavior-equivalence suite, a Newman collection captured once against the running monolith and re-run, completely unmodified, against whatever backend you point it at. Install Newman if you have not already (npm install -g newman), then run the project’s thin wrapper script against the proxy:

demos/demo-equivalence.sh http://localhost:8888

The script is a few lines of bash around a single newman run invocation against tooling/newman/mea.postman_collection.json, parameterized entirely by the baseUrl you pass it; nothing about the collection itself changes whether you point it at the monolith directly (http://localhost:8080), the Review service directly (http://localhost:8081), or the proxy fronting both (http://localhost:8888). A clean run against the proxy reports every assertion passing — smoke checks, a full happy-path checkout, an out-of-stock rejection, a payment-decline rollback, and the Review service’s own read/write contract including its authentication and validation behavior. If any assertion fails, the script’s output names exactly which request and which expectation broke, which is the entire point of externalizing this check into a re-runnable artifact instead of a “looks right to me” judgment call: a failing run here, right now, with nothing else going on, is the cheapest possible place to catch a misconfigured port, an un-migrated table, or a service that silently failed to start, before you carry that mistake into a chapter that assumes a working baseline.

Finding your way around the repository

Before moving on, it is worth a quick orientation pass over the top-level directories you will keep returning to. None of this needs to be memorized — you will have walked every one of these paths by the time you reach Part 3 — but knowing roughly what lives where now will save you a few find commands later.

Directory What’s in it
examples/ One runnable project per hands-on chapter — the monolith, the strangler proxy, each extracted service — committed in full, not as snippets.
tooling/ Shared developer tooling that spans examples, starting with tooling/newman/ — the behavior-equivalence suite’s collection and environments.
demos/ Thin, documented wrapper scripts that drive the examples for a reader — demo-equivalence.sh, demo-cutover.sh — rather than raw tool invocations buried in prose.
infra/ Configuration consumed by compose.yaml — the OpenTelemetry Collector config, Grafana datasource provisioning, and the monolith’s database init scripts.
scripts/ Project-level automation — stack-up.sh / stack-down.sh for the local infrastructure, plus the diagram generator used across chapters.
_docs/ The chapters themselves — this file is _docs/01-prerequisites.md.
_plans/ The project’s own ADLC ledger — decisions.md, build-plan.md, and per-iteration plans — introduced properly as a reader-facing concept in the next chapter.
assets/ Site styling, images, and the paired SVG/Excalidraw diagrams embedded throughout the chapters.

Ports at a glance

You will see this same quartet of addresses recur across most of Part 3 onward, so it is worth having them in one place rather than re-deriving them from compose.yaml and each project’s application.properties every time:

Service Port Source
Monolith (Spring Boot) 8080 examples/00-monolith/src/main/resources/application.yml
Review service (Quarkus) 8081 examples/02-review-service
Strangler proxy (Camel on Quarkus) 8888 examples/01-strangler-proxy, quarkus.http.port
Grafana (LGTM bundle) 3000 compose.yaml; OTLP on 4317/4318, Prometheus on host 19090 (container 9090 — Cockpit owns host 9090 on Fedora Server and RHEL), Loki on 3100, Tempo on 3200

CI runs this same gate

Everything in this chapter also runs unattended in GitHub Actions. The project’s code-ci.yml workflow builds the Review service, applies the monolith’s own committed Flyway SQL directly against a disposable Postgres service container (rather than booting the monolith’s JVM just to get tables), starts the packaged service, and runs the behavior-equivalence suite’s “Review Context Contract” folder against it — failing the job on any non-zero Newman exit code. That workflow was deliberately validated both ways before it was trusted: a temporarily widened validation rule (@Max(5) loosened to @Max(500) on a review rating) made the exact same newman command this job runs fail on cue, proving the gate actually gates rather than merely running green by default, before the change was reverted and the workflow committed. The practical upshot for you, right now, is that the commands in this chapter are not a one-off onboarding ritual distinct from how the project verifies itself — they are the same gate, run by a different caller.

What you learned

  • The toolchain for this book splits into two required pieces (a JDK 25 install and a Maven on your PATH, or the per-project wrapper) and several scoped-later pieces (Quarkus CLI, Camel CLI, Ruby/Bundler) you can defer without losing anything today.
  • Docker Engine with the docker compose plugin is the one container engine for the compose stack, the test containers, and the minikube cluster, on a Fedora or RHEL host, so the image tags pinned in .env are the ones every path actually runs.
  • scripts/stack-up.sh brings up Postgres, Kafka, and the Grafana LGTM bundle from one compose.yaml, gated by real healthchecks rather than a fixed sleep.
  • The behavior-equivalence suite — one Newman collection, parameterized entirely by --baseUrl — is the same check this chapter asks you to run by hand and the one GitHub Actions runs unattended; getting it green here means you are starting Part 3 from a verified baseline, not a hopeful one.

The next chapter, “The Project Ledger,” introduces the three documents this project uses as its own memory across the chapters you are about to read — decisions.md, build-plan.md, and its reconciliation record — and shows you how to keep the same kind of ledger for your own migration.


Verification status: unverified. This chapter’s commands are transcribed directly from the project’s own tracked scripts, pom files, and workflow definitions (compose.yaml, .env.example, scripts/stack-up.sh, scripts/stack-down.sh, demos/demo-equivalence.sh, tooling/newman/README.md, and .github/workflows/code-ci.yml), not improvised for this chapter, but they have not been re-run end-to-end from a clean clone in the authoring loop that produced this page. The highest-risk things for a reader to confirm on a real run: that scripts/stack-up.sh reports all three services healthy on a fresh machine, that the SPRING_DATASOURCE_PASSWORD override above actually matches whatever you set POSTGRES_PASSWORD to in your own .env, and that demos/demo-equivalence.sh http://localhost:8888 reports a fully green run against your own freshly built services before you continue to Chapter 2.

2026-10-09 (DRQ-077): the container toolchain moved from Podman to Docker Engine and every image pin moved to its newest stable release; the commands above reflect that and are equally unverified until re-run.