This guide explains how to build the project step by step as a developer. It is written to show the purpose of each command and file, not just the final result.
Start from the repository root:
cd /home/umer/Distributed-Springboot-Task-ProcessorCreate pom.xml.
Purpose:
- Defines the project as a Maven Java 21 application.
- Uses Spring Boot as the parent build.
- Adds backend dependencies: Web, JDBC, Validation, AMQP, Actuator, Flyway, PostgreSQL, Prometheus metrics, OpenAPI, JUnit, Mockito, and Testcontainers.
- Configures the Spring Boot Maven plugin for running and packaging the app.
- Configures Failsafe so future integration tests named
*IT.javarun duringmvn verify.
Validate Maven can read the project:
mvn -Dmaven.repo.local=.m2/repository testThe -Dmaven.repo.local=.m2/repository option stores downloaded dependencies in
the project workspace. This is useful in restricted environments where Maven
cannot write to ~/.m2.
Create docker-compose.yml.
Purpose:
- Starts PostgreSQL for durable task state.
- Starts RabbitMQ for distributed task delivery.
- Exposes RabbitMQ management UI for local debugging.
- Adds health checks so scripts and CI can tell when services are ready.
Create .env.example.
Purpose:
- Documents local infrastructure credentials.
- Gives developers a template if they want to create their own
.env.
Validate the Compose file:
docker compose configStart local infrastructure:
./scripts/dev-up.shStop local infrastructure:
./scripts/dev-down.shCreate src/main/resources/application.yml.
Purpose:
- Defines the Spring application name.
- Configures PostgreSQL connection properties.
- Enables Flyway database migrations.
- Configures RabbitMQ connection properties.
- Enables graceful shutdown.
- Exposes Actuator endpoints: health, info, metrics, and Prometheus.
- Defines project-specific settings under
task-processor.
Create src/main/resources/logback-spring.xml.
Purpose:
- Switches logs to structured JSON.
- Adds application metadata to every log entry.
- Makes logs easier to search in production tools later.
Create src/main/resources/db/migration/V1__create_task_processing_schema.sql.
Purpose:
tasksstores the authoritative lifecycle state for every task.task_attemptsrecords worker attempts and future retry/failure history.idempotency_keysmaps anIdempotency-Keyto the task created by that key.task_outboxstores messages that must later be published to RabbitMQ.
Important design choice:
- Task creation writes to PostgreSQL first.
- RabbitMQ messages are derived from the outbox later.
- This prevents losing a task if the API creates a row but RabbitMQ is temporarily unavailable.
Create src/main/java/com/umer/taskprocessor/TaskProcessorApplication.java.
Purpose:
- Starts the Spring Boot application.
- Enables component scanning for controllers, services, repositories, and config.
Create src/main/java/com/umer/taskprocessor/config/AppConfig.java.
Purpose:
- Provides a
Clockbean. - Makes time easier to control in tests.
Create src/main/java/com/umer/taskprocessor/config/TaskProcessorProperties.java.
Purpose:
- Binds
task-processor.*settings fromapplication.ymlinto typed Java configuration.
Create src/main/java/com/umer/taskprocessor/domain/TaskStatus.java.
Purpose:
- Defines task lifecycle states such as
QUEUED,RUNNING,SUCCEEDED,FAILED,CANCELLED, andTIMED_OUT. - Provides helper behavior such as
isTerminal().
Create src/main/java/com/umer/taskprocessor/domain/TaskRecord.java.
Purpose:
- Represents a row from the
taskstable. - Keeps database-backed task state explicit and type-safe.
Create files under src/main/java/com/umer/taskprocessor/api/.
Purpose:
CreateTaskRequestvalidates incomingPOST /tasksrequests.TaskResponseshapes one task response returned to API clients.TaskListResponseshapes paginated list responses.
Design note:
- DTOs are separate from database records so API shape can evolve without forcing database structure to leak everywhere.
Create files under src/main/java/com/umer/taskprocessor/idempotency/.
Purpose:
NormalizedTaskRequeststores the canonical form of a create request.IdempotencyRecordrepresents one row inidempotency_keys.RequestHashernormalizes requests and creates a stable SHA-256 hash.
Why hashing matters:
- If a client repeats the same request with the same
Idempotency-Key, return the original task. - If a client reuses the same key for different input, return
409 Conflict.
Create files under src/main/java/com/umer/taskprocessor/repository/.
Purpose:
JsonbMapperconverts Java maps to/from PostgreSQL JSONB columns.TaskRepositoryowns SQL for inserting, reading, listing, and cancelling tasks.IdempotencyRepositoryowns SQL for idempotency lookup and advisory locks.OutboxRepositoryowns SQL for inserting task outbox events.
Important concurrency detail:
IdempotencyRepository.lockKeyForTransaction()usespg_advisory_xact_lock(hashtext(?)).- That means two concurrent requests with the same idempotency key cannot create duplicate tasks.
Create files under src/main/java/com/umer/taskprocessor/service/.
Purpose:
TaskServiceowns transaction boundaries and lifecycle decisions.TaskCreationResulttells the controller whether a create request was new or an idempotent replay.TaskPagecarries paginated list results.
Create task flow:
- Normalize the request.
- Hash the normalized request.
- Lock the idempotency key for the current transaction.
- Check if the key already exists.
- Return the existing task if the hash matches.
- Reject with conflict if the hash differs.
- Insert the task.
- Insert the idempotency record.
- Insert the outbox event.
- Commit all three inserts together.
Create files under src/main/java/com/umer/taskprocessor/web/.
Purpose:
TaskControllerexposes REST endpoints.GlobalExceptionHandlerconverts exceptions intoProblemDetailresponses.CorrelationIdFilteraddsX-Correlation-IDto logs and responses.TaskNotFoundExceptionandIdempotencyConflictExceptiondescribe expected API failures.
Current endpoints:
POST /api/v1/tasks
GET /api/v1/tasks/{id}
GET /api/v1/tasks
POST /api/v1/tasks/{id}/cancel
Create files under scripts/.
Purpose:
dev-up.shstarts PostgreSQL and RabbitMQ.dev-down.shstops local infrastructure.run-tests.shruns the Maven verification suite.submit-task.shsends a sample task creation request withcurl.
Make scripts executable:
chmod +x scripts/dev-up.sh scripts/dev-down.sh scripts/run-tests.sh scripts/submit-task.shCreate src/test/java/com/umer/taskprocessor/idempotency/RequestHasherTest.java.
Purpose:
- Proves equivalent JSON payload ordering creates the same idempotency hash.
- Proves different task input creates a different hash.
- Proves server defaults are included during normalization.
Create support classes under src/test/java/com/umer/taskprocessor/support/.
Purpose:
PostgresTestContainerSupportprovides a reusable PostgreSQL container.RabbitMqTestContainerSupportprovides a reusable RabbitMQ container.- Future integration tests can share these instead of duplicating setup.
Run tests:
mvn -Dmaven.repo.local=.m2/repository testExpected result for the current slice:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Start infrastructure:
./scripts/dev-up.shStart the Spring Boot app:
mvn -Dmaven.repo.local=.m2/repository spring-boot:runCreate a task:
./scripts/submit-task.shList tasks:
curl http://localhost:8080/api/v1/tasksCheck health:
curl http://localhost:8080/actuator/healthCheck Prometheus metrics:
curl http://localhost:8080/actuator/prometheusOpen API documentation:
http://localhost:8080/swagger-ui.html
Update src/main/resources/application.yml.
Purpose:
- Add
task-processor.runtime.api-enabled. - Add
task-processor.runtime.worker-enabled. - Add
task-processor.runtime.outbox-enabled. - Add
task-processor.runtime.scheduling-enabled. - Let the same Spring Boot image run as API-only, worker-only, or all-in-one.
- Let tests or special deployments disable scheduled recovery/outbox loops while still keeping the underlying beans available for direct calls.
Update src/main/java/com/umer/taskprocessor/config/TaskProcessorProperties.java.
Purpose:
- Bind runtime, RabbitMQ, outbox, worker, and retry settings into typed Java records.
- Keep configuration discoverable and compile-time checked.
Update TaskController.
Purpose:
- Add
@ConditionalOnPropertyso the public task API only loads whenapi-enabled=true.
Create src/main/java/com/umer/taskprocessor/config/RabbitMqConfig.java.
Purpose:
- Declares the durable direct exchange.
- Declares the durable task queue.
- Declares a dead-letter queue for rejected malformed messages.
- Binds the task queue to the exchange with the configured routing key.
- Configures JSON message conversion.
- Configures manual acknowledgement and worker prefetch.
Why manual acknowledgement matters:
- The worker only acknowledges after it has safely handled the message.
- If the message is malformed, it is rejected without requeue.
- If the message is duplicate or stale, the worker acknowledges it because PostgreSQL already proves there is no work to do.
Create src/main/java/com/umer/taskprocessor/outbox/OutboxRecord.java.
Purpose:
- Represents a pending row from
task_outbox.
Update src/main/java/com/umer/taskprocessor/repository/OutboxRepository.java.
Purpose:
- Find due pending rows with
FOR UPDATE SKIP LOCKED. - Mark rows as published after RabbitMQ accepts the message.
- Reschedule publish attempts after transient failures.
- Insert retry dispatch events when a task needs another attempt.
Create src/main/java/com/umer/taskprocessor/outbox/OutboxPublisher.java.
Purpose:
- Runs on a schedule.
- Reads due outbox rows.
- Publishes persistent RabbitMQ messages.
- Marks outbox rows as published in PostgreSQL.
Design note:
- RabbitMQ messages contain the task ID, not the full task payload.
- PostgreSQL remains the source of truth.
Create files under src/main/java/com/umer/taskprocessor/worker/.
Purpose:
TaskHandlerdefines the contract for executable task types.TaskExecutionResultwraps handler output.TaskExecutionExceptioncarries failure code and retryability.TaskHandlerRegistryresolves a task type to a handler.ChecksumTaskHandlerimplements theCHECKSUMdemo task.DelayTaskHandlerimplements theDELAYdemo task.RetryPolicycalculates exponential backoff.TaskWorkerServiceowns worker execution flow.TaskMessageListenerconsumes RabbitMQ messages with manual ack.TaskRecoverySchedulerrecovers expired running tasks.
Worker execution flow:
- RabbitMQ delivers a message containing
taskId. - The worker tries to claim the task in PostgreSQL.
- If the task is not claimable, the message is acknowledged and ignored.
- If claimed, the worker inserts a running attempt row.
- The matching handler executes.
- Success stores
resultand marks the taskSUCCEEDED. - Retryable failure schedules
RETRY_SCHEDULEDand inserts a future outbox row. - Non-retryable or exhausted failure marks the task
FAILED. - Expired locks are recovered by the scheduler and either retried or timed out.
Important SQL idea:
- Claiming uses a conditional
UPDATE ... RETURNING. - This makes only one worker able to move a task from
QUEUEDorRETRY_SCHEDULEDtoRUNNING.
Create src/main/java/com/umer/taskprocessor/metrics/TaskProcessingMetrics.java.
Purpose:
- Counts published outbox events.
- Counts publish failures.
- Counts succeeded, failed, retried, and timed-out tasks.
- Exposes a gauge for pending outbox rows.
Check metrics locally:
curl http://localhost:8080/actuator/prometheusCreate Dockerfile.
Purpose:
- Builds the Spring Boot jar with Maven.
- Runs it on a Java 21 runtime image.
Create .dockerignore.
Purpose:
- Keeps build output, local Maven cache, Git metadata, and IDE files out of the Docker build context.
Update docker-compose.yml.
Purpose:
- Add
api,worker,postgres, andrabbitmqservices. - Run API and worker as separate services using the same image.
- Set environment variables so API and worker have different runtime roles.
Build and run the full stack:
./scripts/stack-up.shThe Compose architecture is:
client -> api -> postgres -> task_outbox -> worker publisher -> rabbitmq -> worker consumer -> postgres
Create:
src/test/java/com/umer/taskprocessor/worker/RetryPolicyTest.javasrc/test/java/com/umer/taskprocessor/worker/ChecksumTaskHandlerTest.java
Purpose:
- Verify exponential backoff behavior.
- Verify retry limits.
- Verify
CHECKSUMproduces the expected SHA-256 output. - Verify invalid handler input fails as non-retryable.
Run tests:
mvn -Dmaven.repo.local=.m2/repository testExpected result after this slice:
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Create src/test/java/com/umer/taskprocessor/integration/TaskProcessingIT.java.
Purpose:
- Boots the Spring application on a random port.
- Starts real PostgreSQL and RabbitMQ containers.
- Creates a task through the REST API.
- Waits for the outbox publisher and worker to execute it.
- Verifies final task state, handler result, task attempts, and published outbox rows.
- Verifies idempotent replay and
409 Conflictbehavior against a real database.
Run integration tests:
mvn -Dmaven.repo.local=.m2/repository verifyWhy this matters:
- Unit tests prove local logic.
- Integration tests prove the service can coordinate PostgreSQL transactions, RabbitMQ messages, Flyway migrations, Spring scheduling, and worker consumers.
Create requirements-robot.txt.
Purpose:
- Pins Robot Framework and RequestsLibrary versions.
Create tests/robot/task_api.robot.
Purpose:
- Exercises the service as an external API client.
- Checks health.
- Creates a task and polls until
SUCCEEDED. - Verifies idempotency replay and conflict.
- Verifies validation problem responses.
- Verifies Prometheus metrics are exposed.
Create scripts/run-e2e.sh.
Purpose:
- Creates a local Python test environment.
- Installs Robot dependencies.
- Runs the Robot suite against
BASE_URL.
Run Robot locally:
./scripts/stack-up.sh
./scripts/run-e2e.shRun Robot inside Docker Compose:
docker compose --profile test run --rm robot-testsCreate .gitlab-ci.yml.
Purpose:
unit_testsruns fast Maven tests.integration_testsrunsmvn verifywith Testcontainers and Docker-in-Docker.robot_e2estarts the Compose stack and runs Robot inside the Compose network.package_imagebuilds the Spring Boot jar and Docker image.
Important CI environment variables:
MAVEN_OPTS=-Dmaven.repo.local=.m2/repositorykeeps Maven dependencies cached in the project workspace.DOCKER_HOST=tcp://docker:2375points Testcontainers and Docker Compose at the GitLab Docker-in-Docker service.TESTCONTAINERS_RYUK_DISABLED=trueavoids privileged sidecar requirements in many shared CI runners.
Create scripts/wait-for-url.sh.
Purpose:
- Polls an HTTP endpoint until it is ready.
- Used by CI before running Robot tests.
Create scripts/load-generator.py.
Purpose:
- Submits a batch of sample tasks.
- Polls task states until all tasks become terminal.
- Helps developers observe worker throughput, retries, and metrics locally.
Example:
./scripts/load-generator.py --count 50 --task-type CHECKSUMThe project now accepts, publishes, and executes tasks with automated test layers around the main reliability behavior. Natural next improvements are:
- Authentication and authorization for the REST API.
- More task handler examples with external API calls.
- Dashboarding examples for Prometheus metrics.
- Production deployment manifests for Kubernetes or another orchestrator.