Portfolio-grade QA automation lab for testing a small distributed order system.
This repository is designed for QA Automation Engineers who want practical, interview-ready experience with REST API automation, schema validation, Redis cache/rate-limit behavior, RabbitMQ retry/DLQ flows, Kafka-compatible event processing with Redpanda, and performance testing with k6, JMeter, and Locust.
- Java 21 Spring Boot backend with layered controller/service/repository design.
- PostgreSQL persistence with Flyway migrations and deterministic product seed data.
- REST API validation, consistent error responses, and idempotent order creation.
- Redis order-status cache with TTL, cache-miss fallback, and fixed-window rate limiting.
- RabbitMQ invoice generation using exchange, queue, routing key, retry, and DLQ.
- Redpanda/Kafka
ORDER_CREATEDevent flow with an idempotent notification consumer. - Opt-in black-box RestAssured API tests with Awaitility for async behavior.
- JSON schema validation for event payloads.
- k6, JMeter, and Locust performance assets that exercise the real order flow.
- Offline-safe Maven build and a simple GitHub Actions CI workflow.
Client / API tests / performance tools
|
v
order-api Spring Boot service
|
+-- REST controllers
+-- validation and global error handling
+-- product/order/idempotency services
+-- Redis cache and rate-limit services
+-- RabbitMQ invoice publisher and in-process listener
+-- Kafka ORDER_CREATED publisher and in-process notification listener
|
+--> PostgreSQL
+--> Redis
+--> RabbitMQ invoice.generate.q -> invoice.generate.dlq
+--> Redpanda topic orders.events
The invoice and notification workers run in-process to keep the lab manageable. The messaging boundaries are still real: RabbitMQ and Redpanda run as external Docker services, and the consumers are written so they could later be moved into separate services.
| Area | Tools |
|---|---|
| Backend | Java 21, Spring Boot, Maven |
| API testing | JUnit 5, RestAssured, AssertJ, Awaitility |
| Persistence | PostgreSQL, Flyway, Spring Data JPA |
| Cache/rate limit | Redis, Spring Data Redis |
| Queue | RabbitMQ with management UI, Spring AMQP |
| Streaming | Redpanda as Kafka-compatible broker, Spring Kafka |
| Performance | k6, JMeter, Locust |
| CI | GitHub Actions, Maven verify |
- Java 21
- Maven 3.9+
- Docker Desktop or compatible Docker Compose
- Optional for performance tests: k6, JMeter, Python with Locust
The root build is intentionally offline-safe. It compiles the app and tests, but does not require Docker, PostgreSQL, Redis, RabbitMQ, Redpanda, or a running API.
mvn clean verifyStart the full local stack:
docker compose up --buildOr use the helper script:
.\scripts\start-local.ps1Verify health:
Invoke-RestMethod http://localhost:8080/api/health
Invoke-RestMethod http://localhost:8080/actuator/healthIf port 8080 is busy:
$env:ORDER_API_PORT="18080"
docker compose up --buildThe reset and internal inspection endpoints are disabled by default. Enable them only for local black-box testing:
$env:SPRING_PROFILES_ACTIVE="docker,testtools"
docker compose up --buildOr:
.\scripts\start-testtools.ps1Full black-box API tests are opt-in because they require a live Dockerized stack with testtools enabled.
mvn -pl tests/api-tests test -DrunApiTests=true "-Dapi.base-url=http://localhost:8080"Or:
.\scripts\run-api-tests.ps1You can also use BASE_URL:
$env:BASE_URL="http://localhost:18080"
.\scripts\run-api-tests.ps1Performance tests create real orders, invoices, notifications, Redis keys, RabbitMQ activity, and Kafka events. For clean local results, recreate Docker volumes:
docker compose down -v
$env:SPRING_PROFILES_ACTIVE="docker,performance"
docker compose up --buildOr:
.\scripts\start-performance.ps1k6:
$env:BASE_URL="http://localhost:8080"
k6 run performance/k6/create-order-smoke.js
k6 run performance/k6/create-order-load.js
k6 run performance/k6/create-order-spike.js
k6 run performance/k6/create-order-soak.jsPowerShell wrappers:
.\scripts\run-k6-smoke.ps1
.\scripts\run-k6-load.ps1JMeter:
jmeter -n -t performance/jmeter/create-order-load-test.jmx -l performance-results.jtlLocust:
locust -f performance/locust/locustfile.py --host http://localhost:8080
locust -f performance/locust/locustfile.py --host http://localhost:8080 --headless -u 20 -r 5 -t 2mPerformance tools are not part of the Maven lifecycle.
| Service | Default URL/Port | Notes |
|---|---|---|
| order-api | http://localhost:8080 |
Override with ORDER_API_PORT |
| PostgreSQL | localhost:5432 |
Override with POSTGRES_PORT |
| Redis | localhost:6379 |
Override with REDIS_PORT |
| RabbitMQ AMQP | localhost:5672 |
Override with RABBITMQ_PORT |
| RabbitMQ UI | http://localhost:15672 |
qa_lab_user / qa_lab_password |
| Redpanda Kafka API | localhost:9092 |
Override with REDPANDA_KAFKA_PORT |
| Redpanda Admin | localhost:9644 |
Override with REDPANDA_ADMIN_PORT |
| Redpanda Console | http://localhost:8082 |
Override with REDPANDA_CONSOLE_PORT |
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/api/health |
Lab health response |
GET |
/actuator/health |
Spring Actuator health |
GET |
/api/products/{productId} |
Product lookup |
POST |
/api/orders |
Create order with validation and idempotency |
GET |
/api/orders/{orderId} |
Fetch order |
GET |
/api/orders/{orderId}/status |
Fetch order status from Redis or PostgreSQL fallback |
GET |
/api/invoices/order/{orderId} |
Fetch eventually created invoice |
GET |
/api/notifications/order/{orderId} |
Fetch eventually created notification |
Testtools-only endpoints:
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/test/reset |
Reset lab data while keeping products |
GET |
/api/internal/cache/orders/{orderId} |
Inspect Redis order-status cache |
DELETE |
/api/internal/cache/orders/{orderId} |
Delete one Redis order-status cache key |
GET |
/api/internal/rate-limit/{customerId} |
Inspect Redis rate-limit key |
GET |
/api/internal/rabbitmq/invoice-dlq/count |
Inspect invoice DLQ count |
POST |
/api/internal/rabbitmq/publish-invalid-invoice-job |
Publish invalid invoice job |
POST |
/api/internal/rabbitmq/publish-duplicate-invoice-job |
Publish duplicate invoice job |
GET |
/api/internal/invoices/order/{orderId}/count |
Count invoices for one order |
GET |
/api/internal/notifications/order/{orderId}/count |
Count notifications for one order |
GET |
/api/internal/kafka/processed-events/{eventId} |
Inspect processed Kafka event ledger |
POST |
/api/internal/kafka/publish-duplicate-order-created-event |
Publish duplicate order-created events |
POST |
/api/internal/kafka/publish-invalid-order-created-event |
Publish missing-order event |
{
"customerId": "CUST-1001",
"productId": "PRD-1001",
"quantity": 2,
"paymentMethod": "CARD",
"idempotencyKey": "idem-12345"
}Expected new-order behavior:
- Returns
201. - Stores order in PostgreSQL.
- Writes
order:status:{orderId}to Redis with TTL. - Publishes an invoice job to RabbitMQ after commit.
- Publishes
ORDER_CREATEDto Redpanda after commit. - Creates invoice and notification asynchronously.
Idempotency replay with the same normalized payload returns the same order with 200 and does not republish messages/events. A different payload with the same key returns 409.
mvn clean verifyis safe for local development and CI because external black-box tests are skipped by default.- RestAssured tests run only with
-DrunApiTests=true. - Awaitility waits for async invoice and notification outcomes without hard sleeps.
- Testtools endpoints provide deterministic reset and diagnostics only when
qa-lab.testtools.enabled=true. - k6, JMeter, and Locust are manual performance tools and are not executed by Maven or default CI.
The first CI workflow is intentionally conservative:
checkout -> setup Java 21 with Maven cache -> mvn clean verify
It does not start Docker Compose or run performance tests by default. Dockerized API smoke tests can be added later as a separate job.
- No transactional outbox. If an order commits and broker publishing fails after commit, invoice jobs or order events can be lost.
- Invoice and notification workers are in-process for lab simplicity.
- Kafka topics are append-only in this lab; reset clears database state but does not purge
orders.events. - No production authentication, authorization, TLS, secrets management, or tenant isolation.
- Performance results are local-machine dependent and should be treated as baselines, not production capacity.
- No advanced observability stack yet.
- Testcontainers-based integration tests.
- Allure reporting.
- Dockerized CI integration-test job.
- Optional k6 smoke job in CI.
- Prometheus and Grafana dashboards.
- Split
invoice-workerandnotification-workerinto separate services. - Transactional outbox pattern and relay.
- Schema Registry with Avro or Protobuf.
- Broader contract testing for REST responses and message payloads.
This project demonstrates how I test a distributed order workflow end to end. I start with deterministic REST API behavior, then verify side effects through PostgreSQL, Redis, RabbitMQ, and Redpanda using black-box tests and controlled diagnostics. Async assertions use bounded polling instead of sleeps, idempotency is tested at both API and consumer levels, and performance scripts exercise the real system path rather than isolated mock endpoints.