A distributed workflow automation platform built around a configuration-driven state machine engine — workflows (approval chains, review processes, etc.) are defined as data, not hardcoded Java logic, so new workflow types can be added without touching the engine's code.
Built with Java 21, Spring Boot 3.2, Kafka, PostgreSQL, and Docker, across three independently deployable microservices.
Most portfolio "workflow" projects are CRUD apps with a status column. This one implements an actual state machine: workflow definitions declare states, transitions, role restrictions, and SpEL conditions as JSON, and the engine evaluates them at runtime — including conditional routing (e.g. the same "Approve" action sends a request to Finance review or straight to Approved, depending on the amount).
React dashboard
│
▼
API Gateway (direct REST calls to each service in this version — see Roadmap)
│
├──► workflow-engine-service (8081) ──► PostgreSQL (workflow_engine_db)
│ │ publishes "workflow-events" to Kafka
│ ▼
├──► task-service (8082) ──► PostgreSQL (task_service_db)
│ │ consumes "workflow-events", publishes "task-events"
│ │ calls back into workflow-engine-service when a task is completed
│ │ runs a scheduled SLA-escalation job
│ ▼
└──► notification-service (8083) — no database, in-memory store
consumes "task-events", logs "emails", exposes in-app notifications
| Service | Responsibility | Store |
|---|---|---|
workflow-engine-service |
Owns workflow definitions and instances; runs the state machine; is the source of truth for workflow state | PostgreSQL |
task-service |
Creates human tasks when a workflow enters a state needing action; enforces SLA deadlines; escalates overdue tasks | PostgreSQL |
notification-service |
Turns task events into email/in-app alerts | In-memory (deliberately no DB — see below) |
A workflow definition looks like this (a purchase-order approval flow with amount-based routing):
{
"states": ["DRAFT", "MANAGER_REVIEW", "FINANCE_REVIEW", "APPROVED", "REJECTED"],
"initialState": "DRAFT",
"transitions": [
{ "from": "DRAFT", "to": "MANAGER_REVIEW", "action": "SUBMIT", "allowedRoles": ["REQUESTER"] },
{ "from": "MANAGER_REVIEW", "to": "FINANCE_REVIEW", "action": "APPROVE",
"allowedRoles": ["MANAGER"], "condition": "#amount > 50000" },
{ "from": "MANAGER_REVIEW", "to": "APPROVED", "action": "APPROVE",
"allowedRoles": ["MANAGER"], "condition": "#amount <= 50000" },
{ "from": "MANAGER_REVIEW", "to": "REJECTED", "action": "REJECT", "allowedRoles": ["MANAGER"] },
{ "from": "FINANCE_REVIEW", "to": "APPROVED", "action": "APPROVE", "allowedRoles": ["FINANCE"] },
{ "from": "FINANCE_REVIEW", "to": "REJECTED", "action": "REJECT", "allowedRoles": ["FINANCE"] }
]
}Adding a second workflow (e.g. leave requests) means POSTing a new definition and adding one entry to task-service's task-routing.state-role-map in application.yml — no Java changes.
Its data is transient UI alerts, not the system of record — the audit trail lives in workflow-engine-service's state_transition_logs table. Not every microservice needs its own persistent store; this is a deliberate right-sizing decision, not an oversight.
Prerequisites: Java 21, Maven, Docker & Docker Compose.
# 1. Build all modules
mvn clean install
# 2. Start everything (Postgres x2, Kafka, Zookeeper, all 3 services)
docker-compose up --buildServices come up on localhost:8081 (workflow-engine), 8082 (task-service), 8083 (notification-service).
# 1. Create the purchase-order-approval workflow definition
curl -X POST http://localhost:8081/api/v1/workflow-definitions \
-H "Content-Type: application/json" \
-d '{
"key": "purchase-order-approval",
"name": "Purchase Order Approval",
"definitionJson": {
"states": ["DRAFT","MANAGER_REVIEW","FINANCE_REVIEW","APPROVED","REJECTED"],
"initialState": "DRAFT",
"transitions": [
{"from":"DRAFT","to":"MANAGER_REVIEW","action":"SUBMIT","allowedRoles":["REQUESTER"]},
{"from":"MANAGER_REVIEW","to":"FINANCE_REVIEW","action":"APPROVE","allowedRoles":["MANAGER"],"condition":"#amount > 50000"},
{"from":"MANAGER_REVIEW","to":"APPROVED","action":"APPROVE","allowedRoles":["MANAGER"],"condition":"#amount <= 50000"},
{"from":"MANAGER_REVIEW","to":"REJECTED","action":"REJECT","allowedRoles":["MANAGER"]},
{"from":"FINANCE_REVIEW","to":"APPROVED","action":"APPROVE","allowedRoles":["FINANCE"]},
{"from":"FINANCE_REVIEW","to":"REJECTED","action":"REJECT","allowedRoles":["FINANCE"]}
]
}
}'
# 2. Start an instance
curl -X POST http://localhost:8081/api/v1/workflow-instances \
-H "Content-Type: application/json" \
-d '{"workflowDefinitionKey":"purchase-order-approval","createdByUserId":"babina","context":{"amount":75000}}'
# → note the returned "id", e.g. "abc-123"
# 3. Submit it (DRAFT -> MANAGER_REVIEW)
curl -X POST http://localhost:8081/api/v1/workflow-instances/abc-123/actions \
-H "Content-Type: application/json" \
-d '{"action":"SUBMIT","actorUserId":"babina","actorRole":"REQUESTER"}'
# 4. task-service auto-creates a MANAGER task - check it
curl "http://localhost:8082/api/v1/tasks?role=MANAGER"
# 5. Complete the task as the manager (amount > 50000 -> routes to FINANCE_REVIEW automatically)
curl -X POST http://localhost:8082/api/v1/tasks/{taskId}/complete \
-H "Content-Type: application/json" \
-d '{"action":"APPROVE","actorUserId":"manager1","actorRole":"MANAGER"}'
# 6. See the full audit trail
curl http://localhost:8081/api/v1/workflow-instances/abc-123/history- Backend: Java 21, Spring Boot 3.2, Spring Data JPA, Spring Kafka, Spring Validation
- Messaging: Apache Kafka (event-driven task creation and notifications)
- Databases: PostgreSQL (per-service, polyglot persistence), Flyway migrations
- Build: Maven multi-module
- Containerization: Docker, Docker Compose
- Resilience/consistency choices: synchronous REST for user-triggered actions that need an immediate response (task completion → workflow transition), async Kafka events for side effects (notifications, task creation) that can tolerate eventual consistency
This is a from-scratch build, so a few things are intentionally out of scope for v1 rather than hidden:
- No API Gateway / service discovery (Eureka) yet — services call each other directly via configured URLs. Straightforward to add if the service count grows.
- No React frontend yet — the API is fully functional and demoed above via curl; a task-inbox + workflow-status dashboard is the natural next step.
- No auth/JWT yet —
actorUserId/actorRoleare passed directly in requests. A real deployment needs Spring Security + JWT validating that the caller actually has the claimed role. - SLA escalation currently logs + notifies — it doesn't yet reassign or auto-escalate to a manager's manager.
workflow-automation-platform/
├── common/ # Shared Kafka event contracts (WorkflowInstanceEvent, TaskEvent)
├── workflow-engine-service/ # State machine core, workflow definitions & instances
├── task-service/ # Task creation, SLA tracking, escalation
├── notification-service/ # Event-driven alerts
└── docker-compose.yml