A municipal parking system built as a genuinely distributed application: three client apps over a 3-node MongoDB replica set and a 3-node RabbitMQ quorum-queue cluster, with a recommender tier that reaches agreement by majority-vote consensus. Deployed across a 12-machine cluster.
Developed over three rolling assignments, each building on the last, including red-team / blue-team security rounds against another team's system.
My contribution (full per-assignment breakdown in the tables below): the three client applications and CLI in phase 1; the RabbitMQ quorum-queue cluster and the Docker cluster deployment in phase 2; and the recommendation engine, its malicious-mode toggle and engine tests in phase 3.
Course: Distributed Systems - Semester 2, 5786 (2026) Institution: Kinneret College on the Sea of Galilee Assignment: #3 - Recommender and Consensus (builds on #1 and #2) Team size approved by lecturer: 3 (instead of 4-5)
| Name | Assignment 3 Task | Hours |
|---|---|---|
| Rotem Swisa | Task 2 - Recommender Server logic & Malicious Mode (RecommendationEngine, RecommenderNode, RecommenderServer, engine tests) |
~30 |
| Lidor Ben Simon | Task 3 - Consensus Protocol Engine (ConsensusVoter, ConsensusCoordinator, ClusterConfig) + Task 4 - DevOps, JUnit suite & 12-machine deployment docs |
~30 |
| Libar Vizman | Task 1 - UI/CLI recommender changes (Customer GUI view + CLI recommend) + Task 5 - Blue Teaming code fixes & docs/Defense.md |
~30 |
New components delivered in Assignment 3: a 3-node Recommender Server cluster (SUC-8) with a majority-vote consensus protocol and a per-node malicious mode toggle (GUI + CLI), plus the Round-2 Blue-Team security hardening (secrets out of source, plaintext AMQP off, HMAC + replay + nonce on every node). See
docs/ConsensusProtocol.mdanddocs/Defense.md.
| Name | Assignment 2 Task | Hours |
|---|---|---|
| Rotem Swisa | Task 2 (RabbitMQ 3-node Quorum Queue cluster) + Task 4 (DevOps: docker-compose-cluster.yml, Javadoc, Testing Plan) | ~25 |
| Lidor Ben Simon | Task 1 (Cluster-aware UI APIs with node fallback) + Task 5 (Blue Team: code fixes) | ~25 |
| Libar Vizman | Task 3 (3-node MongoDB replica set) + Task 5 (Blue Team: Defence.md report) | ~25 |
| Name | Assignment 1 Task | Hours |
|---|---|---|
| Rotem Swisa | Task 1 - UI (Customer / PEO / MO GUI + CLI) | ~35 |
| Lidor Ben Simon | Task 3 - Database & Storage (SQLite DAO) | ~35 |
| Libar Vizman | Task 2 + Task 4 - Queue Server, DevOps, Build, Test | ~35 |
For the red-team exercise the team shipped pre-built JARs together with three seeded accounts, one per role — Customer, PEO and MO. Those build artefacts are not part of this repository.
The demo users are seeded in plaintext in DatabaseManager and MongoDBManager
so that a fresh local database is immediately usable for grading and for the
red-team round. They unlock nothing outside a local test database.
In a real deployment this is exactly what you would not do: passwords would be
salted and hashed, and the seed would move out of source into an environment
file. The Assignment 3 blue-team round moved the infrastructure secrets out of
source — the Mongo keyfile and the AMQP credentials now come from .env, and
every node message carries an HMAC signature with replay and nonce protection.
Application-level password hashing was outside the scope of the assignment.
ParkFlow (brand name) is our implementation of the lecturer's "Mulligan" parking system. It has three client applications:
- Customer app - start / stop parking, view history, live parking timer.
- PEO app (Parking Enforcement Officer) - check vehicles, issue fines, review inspection history.
- MO app (Municipality Officer) - reports of all parking payments and citations, including currently-open parkings.
Behind them are two server processes:
- Database server - MongoDB 3-node replica set (
rs0) shared across all clients over TCP. Falls back to a local SQLite file whenMONGO_HOSTSis not set (local development mode). - Queue server - RabbitMQ 3-node cluster with two Quorum Queues:
transactionsandcitations.
All code is Java 21, built with Gradle. See DEPLOY.md for both local and
Dockerised run instructions.
| SUC | Actor | Description | Where |
|---|---|---|---|
| SUC-1 | Customer | Start Parking | Customer → DB |
| SUC-2 | Customer | Stop Parking → Transaction to queue | Customer → DB + RabbitMQ |
| SUC-3 | Customer | Get parking-events list | Customer → DB |
| SUC-4 | PEO | Check whether vehicle is legally parked | PEO → DB (logs to SystemLog + PeoInspections) |
| SUC-5 | PEO | Issue citation | PEO → DB + RabbitMQ |
| SUC-6 | MO | Transaction report | MO → RabbitMQ queue (TCP/AMQP only, no DB) |
| SUC-7 | MO | Citation report | MO → RabbitMQ queue (TCP/AMQP only, no DB) |
| SUC-8 | Customer | Parking Space Recommendation | Customer → Recommender cluster → DB (majority-vote consensus) |
This is the physical architecture from Figure 1 of the assignment, with the new recommender server cluster added between the Customer app and the database.
┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ Customer UI │ │ PEO UI │ │ MO UI │
│ (pub + SUC-8)│ │ (publisher) │ │ (subscriber ONLY) │
└───┬─────┬────┘ └───────┬──────┘ └──────────┬──────────┘
│ │ HMAC-signed TCP │ MongoDB driver │ TCP/AMQP
│ │ (RECOMMENDER_HOSTS │ (MONGO_HOSTS) │ (RABBITMQ_HOSTS)
│ ▼ │ │
│ ┌───────────────────────────────────────────┐ │
│ │ Recommender Server Cluster (SUC-8) │ │
│ │ recommender1 (LEADER) ─ recommender2 ─ │ │
│ │ recommender3 | majority-vote consensus │ │
│ │ + per-node Malicious Mode toggle │ │
│ └───────────────────────┬───────────────────┘ │
│ MongoDB driver │ reads zone snapshot │
▼ ▼ (busy + citation counts) │
┌──────────────────────────────────────┐ │
│ MongoDB Replica Set rs0 │ subscribe │
│ mongo1 (primary) mongo2 mongo3 │◄──────────────────┘
│ WriteConcern=MAJORITY │
│ ReadPreference=primaryPreferred │ ┌───────────────────────────┐
└──────────────────────────────────────┘ │ RabbitMQ 3-node Cluster │
▲ ▲ │ (Quorum Queues) │
│ │ publish (RABBITMQ_HOSTS) │ rabbitmq1 ─ rabbitmq2 ─ │
└───┴────────────────────────────────────► rabbitmq3 │
(Customer + PEO publish transactions/ └───────────────────────────┘
citations to the queue)
Flow for a recommendation (SUC-8): Customer → any recommender node (the
entry node) → the leader collects each node's vote → majority vote → result back
to the Customer. Each node reads the zone's spaces, their busy status, and their
citation counts from MongoDB to compute its vote. See
docs/ConsensusProtocol.md.
Key point: The MO app connects only to RabbitMQ via TCP/AMQP for its SUC-6/SUC-7 reports. (It also reads the DB for the appeals workflow and the "Citations / Space" report, which shows the counts the recommender uses.) Transaction and citation reports themselves come exclusively from the queue.
Failover behaviour
- MongoDB: if the primary goes down, the replica set elects a new primary
within ~10 s. The
ReadPreference.primaryPreferreddriver config falls back to a secondary for reads while the election is in progress. - RabbitMQ:
RABBITMQ_HOSTSis a comma-separated list. The Java client iterates through it and connects to the first reachable node. - UI containers:
MONGO_HOSTSandRABBITMQ_HOSTSenv vars can be set to any subset of nodes; the clients retry automatically.
Important design note - RabbitMQ consumer mode.
In our first iteration we registered consumers on every process that opened
a RabbitMQ connection. That turned out to be a bug: when the PEO app
published a citation, its own background consumer drained it back before the
MO could see it. The fix is explicit subscriber mode: only the MO opens the
connection with consume=true. Everything else is publisher-only.
First-time setup: this repo does NOT ship with the Gradle wrapper JAR (deliberately - it's a binary file). Open the project in IntelliJ once and it will generate
gradle/wrapper/gradle-wrapper.jar,gradlewandgradlew.batfor you. After that, every command below works. Alternatively, rungradle wrapper --gradle-version 8.10from the project root if you have a system Gradle installed.
No Docker required. Uses SQLite locally and a single RabbitMQ node.
# 1. make sure Java 21+ is on your PATH, and Docker Desktop is running
# 2. start RabbitMQ (single node - dev only)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 \
-e RABBITMQ_DEFAULT_USER=parkflow \
-e RABBITMQ_DEFAULT_PASS="Mull1g@nSecure2026!" \
rabbitmq:management
# 3. build
./gradlew build -x test
# 4. run GUI (open three separate terminals)
./gradlew :customer-ui:run
./gradlew :peo-ui:run
./gradlew :mo-ui:run
# 4. run CLI (open three separate terminals)
./gradlew :customer-ui:runCli
./gradlew :peo-ui:runCli
./gradlew :mo-ui:runCliStatus pill in the Customer/PEO/MO window shows Local DB in this mode.
This runs the full Stage-3 feature (SUC-8 + consensus) on one machine: a shared MongoDB, a RabbitMQ node, three recommender servers, and the Customer/PEO/MO clients. The three recommenders must share one database, so we use MongoDB here (not the per-process SQLite fallback).
# 1. infrastructure (shared DB + queue). No auth locally - simplest.
docker run -d --name parkflow-mongo -p 27017:27017 mongo:7
docker run -d --name parkflow-rabbit -p 5672:5672 -p 15672:15672 `
-e RABBITMQ_DEFAULT_USER=parkflow -e "RABBITMQ_DEFAULT_PASS=Mull1g@nSecure2026!" rabbitmq:3.13-management
# 2. build (also produces the recommender fat JAR)
.\gradlew build -x test
.\gradlew :recommender-server:shadowJar -x test
# 3. three recommender nodes - each in its OWN terminal (6001=leader, 6002, 6003).
# Launch with -cp + the CLI class (NOT -jar: the jar's manifest points at the
# JavaFX GUI class which is intentionally not bundled).
java "-DNODE_ID=recommender1" "-DRECOMMENDER_PORT=6001" `
"-DRECOMMENDER_PEERS=recommender1=localhost:6001,recommender2=localhost:6002,recommender3=localhost:6003" `
"-DRECOMMENDER_LEADER=recommender1" "-DMONGO_HOSTS=localhost:27017" `
"-DHMAC_SECRET_KEY=PF-Dev-Local-2026!devOnly" `
-cp recommender-server\build\libs\recommender-server-all.jar com.mulligan.recommender.cli.RecommenderCLI
# ... repeat for recommender2/6002 and recommender3/6003 (change NODE_ID + RECOMMENDER_PORT)
# 4. clients (each in its own terminal), pointed at MongoDB + the recommender cluster
.\gradlew :customer-ui:run "-PMONGO_HOSTS=localhost:27017" "-PRECOMMENDER_HOSTS=localhost:6001,localhost:6002,localhost:6003"
.\gradlew :peo-ui:run "-PMONGO_HOSTS=localhost:27017"
.\gradlew :mo-ui:run "-PMONGO_HOSTS=localhost:27017"In the Customer app open Recommend Parking, enter a zone and a space, and
submit. The leader's terminal prints the live vote tally and the majority
decision. Flip a node to malicious mode by typing malicious on in its terminal
(or use the GUI: :recommender-server:run). The MO app's Citations / Space
page shows the citation counts the recommender uses.
MongoDB Compass (local, no auth): connect with
mongodb://localhost:27017/?directConnection=true(no username/password - the local container runs without auth, unlike the Docker cluster below).
The complete, step-by-step single-machine guide with manual test scenarios and a demo script lives in
docs/DEMO-PRESENTATION.md.
Requires Docker Desktop. Starts the full 12-node cluster (3× MongoDB + 3× RabbitMQ + 3× Recommender + 3× UI).
Assignment 3 secrets note: the cluster no longer ships passwords in the compose file. Run
cp .env.example .envfirst and put your secrets there (rotated values). In the host-run examples below, replace the legacyPFApp2026!mN8x/PFAdmin2026!K9xliterals with theMONGO_APP_PASS/MONGO_ADMIN_PASSvalues from your.env.
# 1. build JARs first (Docker does not run Gradle inside the containers)
./gradlew build -x test
# 2. start the cluster (first run - builds Docker images)
docker compose -f docker-compose-cluster.yml up --build -d
# 2b. subsequent starts (images already built)
docker compose -f docker-compose-cluster.yml up -d
# 3. attach to the CLI sessions inside the containers (in separate terminals)
docker attach parkflow-customer
docker attach parkflow-peo
docker attach parkflow-mo
# To stop and wipe all data volumes (required if you change MongoDB auth):
docker compose -f docker-compose-cluster.yml down -vOr run the GUI / CLI apps on the host with full replica-set failover:
One-time setup - add to
C:\Windows\System32\drivers\etc\hosts(as Admin):127.0.0.1 mongo1 127.0.0.2 mongo2 127.0.0.3 mongo3Each loopback address maps to a different Docker port binding, so the MongoDB driver can resolve all three replica-set members from Windows and automatically failover when any node (including the primary) goes down.
Windows (PowerShell) - GUI:
.\gradlew :customer-ui:run "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :peo-ui:run "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :mo-ui:run "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"Windows (PowerShell) - CLI:
.\gradlew :customer-ui:runCli "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :peo-ui:runCli "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :mo-ui:runCli "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"When the cluster is active you should see
[DB] Using MongoDB cluster: mongo1:27017,...on startup instead of[DB] Using local SQLite database.
Mac/Linux - GUI:
./gradlew :customer-ui:run -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :peo-ui:run -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :mo-ui:run -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'Mac/Linux - CLI:
./gradlew :customer-ui:runCli -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :peo-ui:runCli -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :mo-ui:runCli -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'After adding the hosts file entries above, failover works from both GUI and CLI:
# Stop the primary - GUI and CLI keep working after ~10 s election
docker stop parkflow-mongo1
# Verify new primary was elected
# Note: use single quotes around the password in bash (! triggers history expansion in double quotes)
docker exec parkflow-mongo2 mongosh -u parkflow_admin -p 'PFAdmin2026!K9x' \
--authenticationDatabase admin \
--eval "rs.status().members.map(m => m.name + ' → ' + m.stateStr)" --quiet
# Bring it back
docker start parkflow-mongo1Status pill shows MongoDB Cluster when MONGO_HOSTS is set.
Note - simultaneous failure of 2+ nodes: MongoDB uses Raft consensus. A 3-node replica set requires a majority (≥2) to elect a primary. Losing 2 nodes simultaneously leaves no quorum → no election → writes fail. This is expected and correct behaviour. The system is designed to survive one node failure at a time.
Download from: https://www.mongodb.com/try/download/compass
Connect with this URI (requires the hosts file entries from above):
mongodb://parkflow_app:<MONGO_APP_PASS-from-.env>@mongo1:27017,mongo2:27017,mongo3:27017/parkflow?authSource=admin&replicaSet=rs0
See parkflow → Collections to browse live data during demo.
See DEPLOY.md for the full deployment instructions.
| File | What's in it |
|---|---|
README.md |
This file - overview, team, quick-start. |
DEPLOY.md |
Local and Docker deployment instructions. |
docker-compose-cluster.yml |
Full 12-node cluster (3 Mongo + 3 RabbitMQ + 3 recommender + 3 UIs). |
CLUSTER-DEPLOY-12MACHINES.md |
Standalone bare-metal deployment across 12 physical machines (Assignment 3). |
docs/DEMO-PRESENTATION.md |
Single-machine run + manual test scenarios + demo script for the live presentation. |
docs/ConsensusProtocol.md |
Recommender consensus protocol design (messages, steps, 8 scenarios). |
docs/Defense.md |
Blue Team report: Round-2 root-cause + fixes (R2-C/I/CIA/A series). |
.env.example |
Template for the gitignored .env holding all rotated secrets. |
docs/DatabaseDesign.md |
SQLite schema, indexes, key queries. |
docs/QueueServerDesign.md |
RabbitMQ queues, publisher vs subscriber mode, message format. |
docs/TestingPlan.md |
Acceptance test tables - one per SUC. |
docs/TestScenarios.md |
End-to-end walkthroughs for GUI and CLI. |
Logos and illustrations produced by the team. RabbitMQ is used under the Mozilla Public License 2.0. SQLite is public domain. JavaFX is licensed under GPL v2 with Classpath Exception.
