Lab 03 — Multi-module Maven Reactor
Phase: 07 — Production Systems
Difficulty: ⭐⭐⭐☆☆ | Estimated Time: 1.5–2 hours
Artifacts: 3-module Maven reactor — queue-api, queue-core, queue-app
Command: mvn verify
Background
A single-module Maven project scales fine to ~5,000 lines of code. Beyond that, you start hitting real problems:
| Problem | Root cause | Multi-module fix |
|---|---|---|
| Everything recompiles on every change | One source tree | Modules only recompile when their inputs change |
| Dependency versions drift | Each dev adds a version independently | <dependencyManagement> pins versions once |
| You can't publish an API jar without implementation details | All code in one artifact | api module has zero implementation deps |
| CI builds the whole world for a 1-line typo fix | No incremental build | mvn -pl queue-api -am builds only what changed |
Apache's major projects (Kafka, Spark, Flink, Hadoop) all use this pattern — typically 5–40 modules depending on the project's size.
Module Structure
lab-03-multimodule-maven/ ← parent POM (packaging=pom)
├── pom.xml ← declares modules, dependencyManagement, pluginManagement
├── ci.yml ← GitHub Actions workflow (copy to .github/workflows/)
├── queue-api/ ← interfaces + value objects only
│ ├── pom.xml
│ └── src/main/java/org/example/queue/api/
│ ├── Message.java ← immutable value object
│ └── MessageQueue.java ← stable API interface
├── queue-core/ ← implementations (depends on queue-api)
│ ├── pom.xml
│ └── src/main/java/org/example/queue/core/
│ ├── BoundedFifoQueue.java
│ └── PriorityMessageQueue.java
└── queue-app/ ← composition root + integration tests (depends on queue-core)
├── pom.xml
└── src/
├── main/java/org/example/queue/app/Main.java
└── test/java/org/example/queue/app/QueueIntegrationTest.java
Dependency DAG (Maven resolves build order from this):
queue-api ← queue-core ← queue-app
Step 1 — Understand the Parent POM
Read pom.xml. Find the answers to these questions:
- What is the significance of
<packaging>pom</packaging>? - What is the difference between
<dependencies>and<dependencyManagement>? Add<dependencies>to the parent POM and enqueue a message in a test — what breaks? - What Maven lifecycle phase does
mvn verifyrun through?
Answers
-
packaging=pomtells Maven: "this artifact is not compiled, there is no jar. Its purpose is to declare child modules and shared configuration." -
<dependencyManagement>pins versions but does NOT add the dependency to any module's classpath. Child modules must still declare the dependency; they just omit<version>. If you add<dependencies>to the parent, the dependency appears on every child module's classpath — including modules that don't need it. -
mvn verifyruns: validate → initialize → compile → test → package → verify. It produces jars AND runs all tests, including integration tests.mvn teststops after the test phase (no jar produced).
Step 2 — Trace the Dependency Graph
Run the effective-pom command to see what Maven resolves for queue-app:
cd queue-app && mvn help:effective-pom | grep -A3 "queue-api\|queue-core\|junit"
Verify that:
queue-apiappears as a transitive dependency ofqueue-app(not declared in queue-app/pom.xml)- All JUnit and internal dependencies have resolved versions
Step 3 — Build and Test the Full Reactor
cd lab-03-multimodule-maven
mvn verify
Watch the reactor output. It processes modules in dependency order:
queue-parent → queue-api → queue-core → queue-app
Expected output:
[INFO] Reactor Build Order:
[INFO] Building queue-parent 1.0-SNAPSHOT [1/4]
[INFO] Building queue-api 1.0-SNAPSHOT [2/4]
[INFO] Building queue-core 1.0-SNAPSHOT [3/4]
[INFO] Building queue-app 1.0-SNAPSHOT [4/4]
...
[INFO] Tests run: 13, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Step 4 — Break the Dependency Direction
Try adding this to queue-api/pom.xml:
<dependency>
<groupId>org.example.queue</groupId>
<artifactId>queue-core</artifactId>
</dependency>
Then run mvn verify again. What error do you see?
This is the Maven way of enforcing acyclic dependencies — if api depended on core, the DAG would have a cycle and Maven would fail to build.
Undo this change before proceeding.
Step 5 — Simulate a Version Upgrade
Bump the JUnit version in the parent pom.xml from 5.10.2 to 5.11.0:
<junit.version>5.11.0</junit.version>
Run mvn verify again. Maven downloads 5.11.0 once and uses it for all child modules.
All 13 tests still pass because JUnit 5.x is backward-compatible within minor versions.
Revert to 5.10.2 after verifying.
Key insight: In a 20-module project, this one-line change in the parent POM
upgrades JUnit for ALL modules consistently. Without <dependencyManagement>,
you'd need to update 20 files and risk inconsistency.
Step 6 — Understand the CI Workflow
Read ci.yml. The workflow:
- Checks out the repo
- Sets up JDK 11 (Temurin = Eclipse Adoptium) with Maven cache
- Runs
mvn verify --batch-modefrom the lab directory - Uploads jars as build artifacts
To use this in a real project, copy it to .github/workflows/ci.yml at your
repository root and adjust the working-directory path.
Source Files
Parent pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
Parent POM: packaging=pom means this artifact is never a jar.
Its job is to:
1. Declare which child modules exist (<modules>)
2. Pin all dependency versions in one place (<dependencyManagement>)
3. Configure plugins once for all children (<build><pluginManagement>)
-->
<groupId>org.example.queue</groupId>
<artifactId>queue-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!--
Reactor: Maven builds these modules in dependency order, not declaration order.
It reads each child pom.xml, builds a DAG, then executes: api → core → app.
-->
<modules>
<module>queue-api</module>
<module>queue-core</module>
<module>queue-app</module>
</modules>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Single place to update the JUnit version for ALL child modules -->
<junit.version>5.10.2</junit.version>
</properties>
<!--
dependencyManagement: pins versions but does NOT add them to any module's
classpath. Child modules must still declare the dependency — they just omit
the <version> tag and Maven resolves it from here.
This is the key mechanism that prevents version drift across 10+ modules.
-->
<dependencyManagement>
<dependencies>
<!-- Internal cross-module dependencies — pinned to this project's version -->
<dependency>
<groupId>org.example.queue</groupId>
<artifactId>queue-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.example.queue</groupId>
<artifactId>queue-core</artifactId>
<version>${project.version}</version>
</dependency>
<!--
JUnit: we use junit-jupiter which is an aggregator artifact that pulls in
junit-jupiter-api (for @Test) and junit-jupiter-engine (for test execution).
Declaring it once here means ALL child modules share the same JUnit version.
-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<!--
pluginManagement: configures plugins once. Children inherit the version
and config without repeating it.
-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
ci.yml
# Note: In a real repository, this file lives at .github/workflows/ci.yml
# relative to the repository root. It is included here for review and learning.
# Copy it to .github/workflows/ci.yml to activate it on GitHub.
name: CI
on:
push:
branches: ["main", "master"]
pull_request:
jobs:
build:
name: Build and Test (Java 11)
runs-on: ubuntu-latest
steps:
# 1. Check out the repository
- name: Checkout
uses: actions/checkout@v4
# 2. Set up Java 11 (Temurin = Eclipse Adoptium, the most common free JDK)
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
# Cache ~/.m2/repository to speed up subsequent builds.
# Maven's local repo (~250 MB) is keyed on pom.xml hashes.
cache: maven
# 3. Build the full reactor: api → core → app, run all tests
# --batch-mode: suppresses ANSI progress bars (better for CI logs)
# verify: runs compile → test → package → integration-test phases
- name: Build and test (full reactor)
run: mvn verify --batch-mode
working-directory: PMC-Apache-Comitter-Engineer/phase-07-production-systems/lab-03-multimodule-maven
# 4. Upload the built jars as artifacts (visible in the GitHub Actions UI)
- name: Upload jar artifacts
uses: actions/upload-artifact@v4
if: success()
with:
name: queue-jars
path: |
PMC-Apache-Comitter-Engineer/phase-07-production-systems/lab-03-multimodule-maven/**/target/*.jar
retention-days: 7
queue-api/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
<parent> links this module to the parent POM.
Maven resolves the parent from the local reactor (if building from the parent)
or from ~/.m2/repository if built standalone.
-->
<parent>
<groupId>org.example.queue</groupId>
<artifactId>queue-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!--
Only artifactId is needed — groupId and version are inherited from the parent.
This is one of the key benefits of multi-module: child POMs are minimal.
-->
<artifactId>queue-api</artifactId>
<!--
queue-api is intentionally thin: it contains only interfaces and value objects.
It has NO runtime dependencies on other modules.
Other modules depend on it; it depends on nothing.
This is the "API module" pattern: code that changes rarely, stable contracts,
no implementation details. Publishing this as a separate artifact lets
third-party code depend on the contract without pulling in the implementation.
-->
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<!-- No <version> here — resolved from parent's dependencyManagement -->
</dependency>
</dependencies>
</project>
Message.java
package org.example.queue.api;
/**
* An immutable message that can be enqueued and dequeued.
*
* <p>
* Messages carry a {@code priority} value: higher = more urgent.
* {@link org.example.queue.core.PriorityMessageQueue} uses this to determine
* dequeue order; {@link org.example.queue.core.BoundedFifoQueue} ignores it.
*/
public final class Message {
private final String id;
private final String body;
private final int priority; // higher value = higher urgency
public Message(String id, String body, int priority) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must not be blank");
}
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.id = id;
this.body = body;
this.priority = priority;
}
public String getId() {
return id;
}
public String getBody() {
return body;
}
public int getPriority() {
return priority;
}
@Override
public String toString() {
return "Message{id='" + id + "', priority=" + priority + ", body='" + body + "'}";
}
}
MessageQueue.java
package org.example.queue.api;
import java.util.List;
/**
* A queue of {@link Message}s.
*
* <p>
* Implementations must be thread-safe (see
* {@link org.example.queue.core.BoundedFifoQueue} and
* {@link org.example.queue.core.PriorityMessageQueue}).
*
* <p>
* This is the stable API contract. Callers depend on this interface, not on
* any implementation class. Switching from FIFO to priority ordering requires
* only a one-line change at the construction site, not a refactor of all
* callers.
*/
public interface MessageQueue {
/**
* Adds a message to the queue.
*
* @param message the message to enqueue; must not be null
* @throws IllegalArgumentException if message is null
* @throws IllegalStateException if the queue is at capacity (bounded queues
* only)
*/
void enqueue(Message message);
/**
* Removes and returns the next message according to the queue's ordering
* policy.
* Returns {@code null} if the queue is empty (no blocking).
*
* @return the next message, or {@code null} if empty
*/
Message dequeue();
/** Returns the number of messages currently in the queue. */
int size();
/** Returns {@code true} if the queue contains no messages. */
boolean isEmpty();
/**
* Removes and returns all messages in the queue in dequeue order.
* The queue is empty after this call.
*
* @return all messages, possibly empty but never null
*/
List<Message> drainAll();
}
queue-core/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example.queue</groupId>
<artifactId>queue-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>queue-core</artifactId>
<!--
queue-core depends on queue-api at runtime.
It implements the MessageQueue interface defined there.
Notice there is NO <version> on the queue-api dependency — the version
is resolved from the parent's <dependencyManagement> section.
When we release version 2.0, we change one line in the parent pom.xml,
and all child modules automatically use the new version.
-->
<dependencies>
<dependency>
<groupId>org.example.queue</groupId>
<artifactId>queue-api</artifactId>
<!-- version resolved from parent dependencyManagement -->
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
</project>
BoundedFifoQueue.java
package org.example.queue.core;
import org.example.queue.api.Message;
import org.example.queue.api.MessageQueue;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* A thread-safe, capacity-bounded FIFO queue.
*
* <p>
* Messages are dequeued in the order they were enqueued (first in, first out).
* The {@link Message#getPriority()} field is ignored.
*
* <p>
* When the queue reaches its capacity, {@link #enqueue(Message)} throws
* {@link IllegalStateException} rather than blocking. For backpressure-aware
* use cases, callers should check {@link #size()} before enqueuing or catch
* the exception and retry.
*
* <p>
* All public methods are {@code synchronized} on {@code this}.
* For high-concurrency workloads, prefer
* {@code java.util.concurrent.LinkedBlockingDeque}.
*/
public class BoundedFifoQueue implements MessageQueue {
private final Deque<Message> queue;
private final int capacity;
/**
* @param capacity maximum number of messages the queue can hold
* @throws IllegalArgumentException if capacity <= 0
*/
public BoundedFifoQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be > 0");
}
this.capacity = capacity;
this.queue = new ArrayDeque<>(capacity);
}
@Override
public synchronized void enqueue(Message message) {
if (message == null) {
throw new IllegalArgumentException("message must not be null");
}
if (queue.size() >= capacity) {
throw new IllegalStateException(
"Queue is full (capacity=" + capacity + ")");
}
queue.addLast(message);
}
@Override
public synchronized Message dequeue() {
return queue.pollFirst(); // returns null if empty
}
@Override
public synchronized int size() {
return queue.size();
}
@Override
public synchronized boolean isEmpty() {
return queue.isEmpty();
}
@Override
public synchronized List<Message> drainAll() {
List<Message> result = new ArrayList<>(queue);
queue.clear();
return result;
}
}
PriorityMessageQueue.java
package org.example.queue.core;
import org.example.queue.api.Message;
import org.example.queue.api.MessageQueue;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
/**
* A thread-safe, unbounded priority queue.
*
* <p>
* Messages are dequeued in descending priority order: the message with the
* highest {@link Message#getPriority()} value is dequeued first.
* Messages with equal priority are dequeued in unspecified (heap) order.
*
* <p>
* This is unbounded — there is no capacity limit. For bounded behavior,
* use {@link BoundedFifoQueue}.
*
* <p>
* Backed by {@link java.util.PriorityQueue} with a reversed comparator so
* high-priority messages bubble to the head.
*/
public class PriorityMessageQueue implements MessageQueue {
// Reversed so that higher priority value = polled first
private final PriorityQueue<Message> pq = new PriorityQueue<>(
Comparator.comparingInt(Message::getPriority).reversed());
@Override
public synchronized void enqueue(Message message) {
if (message == null) {
throw new IllegalArgumentException("message must not be null");
}
pq.offer(message);
}
@Override
public synchronized Message dequeue() {
return pq.poll(); // returns null if empty
}
@Override
public synchronized int size() {
return pq.size();
}
@Override
public synchronized boolean isEmpty() {
return pq.isEmpty();
}
/**
* Drains all messages in priority order (highest priority first).
*/
@Override
public synchronized List<Message> drainAll() {
List<Message> result = new ArrayList<>(pq.size());
while (!pq.isEmpty()) {
result.add(pq.poll());
}
return result;
}
}
queue-app/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example.queue</groupId>
<artifactId>queue-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>queue-app</artifactId>
<!--
queue-app depends on queue-core (which transitively brings in queue-api).
The app module is the "composition root" — it's the only place that knows
which implementations are in use.
-->
<dependencies>
<dependency>
<groupId>org.example.queue</groupId>
<artifactId>queue-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!--
maven-jar-plugin: produces an executable jar with a Main-Class manifest entry.
Run with: java -jar target/queue-app-1.0-SNAPSHOT.jar
Note: this only works if all dependencies are bundled (use maven-shade-plugin
for a fat jar with dependencies). Here we keep it simple — just the manifest.
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>org.example.queue.app.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Main.java
package org.example.queue.app;
import org.example.queue.api.Message;
import org.example.queue.api.MessageQueue;
import org.example.queue.core.BoundedFifoQueue;
import org.example.queue.core.PriorityMessageQueue;
/**
* Demonstrates both MessageQueue implementations.
*
* <p>
* This is the "composition root" — the only class that imports concrete
* implementation types ({@link BoundedFifoQueue},
* {@link PriorityMessageQueue}).
* All other code works with the {@link MessageQueue} interface.
*
* <p>
* Run from the parent directory:
*
* <pre>
* mvn package -pl queue-app -am
* java -cp queue-app/target/queue-app-1.0-SNAPSHOT.jar:queue-core/target/queue-core-1.0-SNAPSHOT.jar:queue-api/target/queue-api-1.0-SNAPSHOT.jar org.example.queue.app.Main
* </pre>
*/
public class Main {
public static void main(String[] args) {
System.out.println("=== BoundedFifoQueue Demo ===");
demonstrateFifo();
System.out.println("\n=== PriorityMessageQueue Demo ===");
demonstratePriority();
}
private static void demonstrateFifo() {
MessageQueue queue = new BoundedFifoQueue(5);
queue.enqueue(new Message("msg-1", "First in", 1));
queue.enqueue(new Message("msg-2", "Second in", 3));
queue.enqueue(new Message("msg-3", "Third in", 2));
System.out.println("Enqueued 3 messages. FIFO order (ignores priority):");
while (!queue.isEmpty()) {
System.out.println(" " + queue.dequeue());
}
// Show capacity enforcement
BoundedFifoQueue small = new BoundedFifoQueue(2);
small.enqueue(new Message("a", "one", 1));
small.enqueue(new Message("b", "two", 1));
try {
small.enqueue(new Message("c", "three", 1));
} catch (IllegalStateException e) {
System.out.println("Capacity enforced: " + e.getMessage());
}
}
private static void demonstratePriority() {
MessageQueue queue = new PriorityMessageQueue();
queue.enqueue(new Message("alert-1", "Low priority alert", 1));
queue.enqueue(new Message("alert-2", "High priority alert", 10));
queue.enqueue(new Message("alert-3", "Medium priority alert", 5));
queue.enqueue(new Message("alert-4", "Critical alert", 100));
System.out.println("Enqueued 4 messages. Priority order (highest first):");
while (!queue.isEmpty()) {
System.out.println(" " + queue.dequeue());
}
}
}
QueueIntegrationTest.java
package org.example.queue.app;
import org.example.queue.api.Message;
import org.example.queue.api.MessageQueue;
import org.example.queue.core.BoundedFifoQueue;
import org.example.queue.core.PriorityMessageQueue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests that exercise the full module stack:
* queue-app → queue-core → queue-api
*
* <p>
* These tests live in queue-app because they use concrete implementation
* classes
* ({@link BoundedFifoQueue}, {@link PriorityMessageQueue}) from queue-core and
* verify
* that the entire dependency chain is wired correctly.
*
* <p>
* If queue-api changes its {@link org.example.queue.api.MessageQueue} interface
* in a binary-incompatible way (e.g., adding a default method), this test suite
* will catch it during the reactor build: queue-core fails to compile →
* queue-app
* tests never run → reactor reports BUILD FAILURE.
*/
class QueueIntegrationTest {
/** Provides both implementations for parameterized contract tests. */
static Stream<MessageQueue> implementations() {
return Stream.of(
new BoundedFifoQueue(100),
new PriorityMessageQueue());
}
// =========================================================================
// Shared contract tests (parameterized over both implementations)
// =========================================================================
@ParameterizedTest
@MethodSource("implementations")
void testEnqueueAndDequeue_basicCycle(MessageQueue queue) {
queue.enqueue(new Message("m1", "hello", 1));
assertEquals(1, queue.size());
Message out = queue.dequeue();
assertNotNull(out);
assertEquals("m1", out.getId());
assertTrue(queue.isEmpty());
}
@ParameterizedTest
@MethodSource("implementations")
void testDequeueOnEmpty_returnsNull(MessageQueue queue) {
assertNull(queue.dequeue(),
"dequeue() on an empty queue must return null, not throw");
}
@ParameterizedTest
@MethodSource("implementations")
void testDrainAll_returnsAllAndClearsQueue(MessageQueue queue) {
queue.enqueue(new Message("m1", "a", 1));
queue.enqueue(new Message("m2", "b", 2));
queue.enqueue(new Message("m3", "c", 3));
List<Message> drained = queue.drainAll();
assertEquals(3, drained.size());
assertTrue(queue.isEmpty(), "Queue must be empty after drainAll()");
}
@ParameterizedTest
@MethodSource("implementations")
void testEnqueueNull_throws(MessageQueue queue) {
assertThrows(IllegalArgumentException.class,
() -> queue.enqueue(null),
"Enqueuing null must throw IllegalArgumentException");
}
// =========================================================================
// BoundedFifoQueue-specific tests
// =========================================================================
/** FIFO: messages must dequeue in insertion order, regardless of priority. */
@Test
void testFifo_insertionOrderIsPreserved() {
BoundedFifoQueue queue = new BoundedFifoQueue(10);
for (int i = 1; i <= 5; i++) {
// Enqueue in order 1..5; priority is deliberately descending to show
// that FIFO ignores priority
queue.enqueue(new Message("m" + i, "body", 6 - i));
}
for (int i = 1; i <= 5; i++) {
assertEquals("m" + i, queue.dequeue().getId(),
"FIFO must dequeue m" + i + " at position " + i);
}
}
@Test
void testFifo_capacityEnforced() {
BoundedFifoQueue queue = new BoundedFifoQueue(2);
queue.enqueue(new Message("m1", "a", 1));
queue.enqueue(new Message("m2", "b", 1));
assertThrows(IllegalStateException.class,
() -> queue.enqueue(new Message("m3", "c", 1)),
"Enqueueing beyond capacity must throw IllegalStateException");
}
@Test
void testFifo_dequeueAfterPartialDrain() {
BoundedFifoQueue queue = new BoundedFifoQueue(5);
queue.enqueue(new Message("m1", "a", 1));
queue.enqueue(new Message("m2", "b", 1));
queue.enqueue(new Message("m3", "c", 1));
assertEquals("m1", queue.dequeue().getId()); // free up one slot
queue.enqueue(new Message("m4", "d", 1)); // should succeed now
assertEquals("m2", queue.dequeue().getId());
assertEquals("m3", queue.dequeue().getId());
assertEquals("m4", queue.dequeue().getId());
}
// =========================================================================
// PriorityMessageQueue-specific tests
// =========================================================================
@Test
void testPriority_highestPriorityFirst() {
PriorityMessageQueue queue = new PriorityMessageQueue();
queue.enqueue(new Message("low", "low priority", 1));
queue.enqueue(new Message("high", "high priority", 100));
queue.enqueue(new Message("mid", "mid priority", 50));
assertEquals("high", queue.dequeue().getId(), "Highest priority must dequeue first");
assertEquals("mid", queue.dequeue().getId(), "Mid priority must dequeue second");
assertEquals("low", queue.dequeue().getId(), "Lowest priority must dequeue last");
}
@Test
void testPriority_drainAllIsInPriorityOrder() {
PriorityMessageQueue queue = new PriorityMessageQueue();
queue.enqueue(new Message("c", "third", 1));
queue.enqueue(new Message("a", "first", 10));
queue.enqueue(new Message("b", "second", 5));
List<Message> drained = queue.drainAll();
assertEquals("a", drained.get(0).getId(), "drainAll() position 0 must be highest priority");
assertEquals("b", drained.get(1).getId(), "drainAll() position 1 must be mid priority");
assertEquals("c", drained.get(2).getId(), "drainAll() position 2 must be lowest priority");
}
}