Skip to content
All projects
Academic projectCompleted2026BackendWebCloud

A cloud-native pay-parking platform built as a work-placement project. Drivers scan a QR sign, book without an account and pay with Stripe; wardens check plates; council staff run the car parks from an admin console. Modelled on Enniscorthy's published 2025 parking schedule.

Academic work-placement project built at the Software Research Institute, Technological University of the Shannon (TUS). It is based on Wexford County Council's published 2025 Pay Parking Schedule for Enniscorthy. It was not commissioned by, affiliated with or operated for Wexford County Council, and the public never used it. All payments ran in Stripe test mode.

Driver app map showing the 36 pay-parking locations in Enniscorthy
  • 8 Spring Boot domain services on Java 25 behind a Spring Cloud Gateway, plus 3 React and TypeScript apps for drivers, wardens and council staff.
  • All 36 parking locations in the 2025 schedule (9 car parks and 27 on-street zones), signed by 52 QR codes. Drivers book with no app or account.
  • Stripe payments with signature-verified, deduplicated webhooks. A retried webhook can never create a second session, receipt or charge.
  • Vehicle registrations, emails and phone numbers encrypted at rest with AES-256-GCM. Plates also carry a keyed hash, so wardens can check a plate without decrypting anything.
  • A self-directed penetration test of the cloud demo found 21 issues (1 critical, 3 high). All were fixed across 11 repositories, with 45 new regression tests.
  • An end-to-end campaign completed a full paid booking on all 52 QR codes, checked against Stripe's test dashboard and real inbox delivery.

The problem

Enniscorthy’s pay parking, as set out in Wexford County Council’s published 2025 Pay Parking Schedule No. 2, covers 9 car parks and 27 on-street zones. The placement brief was to design and build a cloud-native platform for three groups of people:

  • Drivers should be able to scan a sign, pay and get a receipt without installing an app or creating an account.
  • Wardens need a way to check payments and issue notices.
  • Council staff need a console to run the whole estate.

This was an academic project. It was never operated for the council, it had no public users, and every payment ran in Stripe test mode.

What was built

  • Driver web app.
    • Each QR sign opens a page for its car park. Drivers can also find parking on a map.
    • The booking form has a confirm-details step before the Stripe payment form (card, Google Pay or Apple Pay).
    • After payment, a permanent receipt page shows a live countdown.
  • Receipts. Each booking uses exactly one channel: email with a PDF, SMS, a Google Wallet pass, an Apple Wallet pass, or Stripe’s hosted receipt.
  • SMS booking. For drivers without a smartphone: register once and save a card, then text a short command to get a paid booking and receipt in the same SMS thread.
  • Warden app.
    • Wardens sign in with a daily SMS link plus a 6-digit code, which locks after 5 wrong attempts.
    • Plate checks show paid, resident permit or unpaid.
    • Wardens can issue a fixed-charge notice and print it on a Bluetooth thermal printer from the browser.
    • Plates can be read with an automatic number-plate recognition (ANPR) service. The platform keeps a scan photo only when a fine is issued.
  • Admin console.
    • Staff sign in through Keycloak with two-factor authentication.
    • A dashboard shows the key figures.
    • A new car park becomes bookable immediately after it is added.
    • Staff can manage resident permits, warden rosters, QR signs and the audit log.
    • Refunds and other sensitive actions require signing in again.

Architecture

  • 20 repositories. A shared library, database migrations, Terraform, the Keycloak realm, 11 backend deployables, 3 single-page apps, plus platform and documentation repositories.
  • Gateway. Spring Cloud Gateway handles routing, booking-token checks, rate limits, request-size limits and a circuit breaker on each route.
  • Domain services:
    • carpark: catalogue and bookings
    • payment: Stripe
    • notification: email, SMS, push and PDF
    • wallet: Google and Apple passes
    • warden: plate checks, ANPR and fines
    • admin: the console backend
    • scheduler: Quartz jobs
    • audit: an append-only log built from events
  • Data. PostgreSQL 17 holds a catalogue of the 36 locations and the 52 QR codes, with separate transaction and receipt tables for each car park. Redis holds token revocations and rate-limit state. RabbitMQ carries events, with a dead-letter queue.
  • Sign-in. Each group of people signs in differently:
    • Drivers get a short-lived anonymous booking token, kept in memory only.
    • Wardens use the SMS link and code.
    • Staff use Keycloak, with OAuth2, PKCE and two-factor codes.
    • Services authenticate to each other with an internal token.

Engineering details

Payments without double charges

  • A payment is created only after a pending booking exists. The receipt ID doubles as Stripe’s idempotency key, so a retried request returns the same payment.
  • Webhooks are verified against Stripe’s signature header, and each event is processed at most once.
  • A booking can leave the pending state only once, and an integration test checks that a redelivered event changes nothing.
  • A database advisory lock on the car park and plate prevents two overlapping bookings for the same car.

Adding car parks at runtime

  • A single locked-down database function adds a car park in one transaction. It creates the catalogue row, the transaction and receipt tables, the receipt sequence and the QR rows.
  • By design, the application’s database role may only call that function and cannot create or drop tables. The local stack and the demo used a single database role, so this separation was not enforced there.
  • Bookings for car parks added later work with no code change.

Security and data protection

  • Build-time checks. An ArchUnit test fails the build if any admin endpoint is missing an authorisation rule.
  • Startup checks. A service refuses to start if its secrets are placeholders or if webhook signature checks are turned off.
  • Encryption. Registration, email and phone are encrypted with AES-256-GCM. Plates, and the phone numbers of SMS drivers, also carry an HMAC-SHA256 hash with its own secret, used for lookups.
  • Retention. A nightly job replaces expired personal data with a redacted placeholder, so financial records stay intact.
  • Erasure. A right-to-erasure endpoint finds a person’s records through their hashed identifiers, without decrypting anyone else’s data.

Reliable events

  • Spring Modulith’s event registry acts as a transactional outbox to RabbitMQ, and consumers ignore duplicate events.
  • Finding. All services shared one outbox table, so restarting one service re-ran the other services’ pending events. Each restart used up one of another service’s retry attempts. Two unrelated deploys could therefore permanently stop a receipt from being delivered.
  • Fix. Each service now has its own outbox schema, with a migration that moves pending events to the right owner.

Infrastructure and delivery

  • Cloud demo. The demo is defined in Terraform as 13 Azure Container Apps (9 backends, 3 web apps and Keycloak), with managed PostgreSQL, Redis and RabbitMQ.
  • CI. GitHub Actions runs unit tests, Testcontainers integration tests and frontend tests, scans dependencies and images with Trivy, and signs container images with cosign.
  • Monitoring. Prometheus alerts cover services going down, dead-letter queue depth and open circuit breakers. Grafana dashboards show request rates, errors, durations and JVM health.

Testing and verification

  • Test types:
    • unit tests
    • integration tests against a real PostgreSQL 17 database (Testcontainers)
    • architecture rules (ArchUnit)
    • coverage gates of 75% overall and 90% on critical packages (JaCoCo)
    • frontend tests
    • a Playwright browser journey
  • End-to-end campaign. A full paid booking was made on each of the 52 QR codes, with every receipt sent to a real inbox. Each payment was cross-checked against Stripe’s test dashboard. The campaign raised seven findings: six real defects, all fixed, and one business rule working as designed.
  • Penetration test. A self-directed test of the cloud demo, run with no credentials, made 171 requests. From 27 suspected issues it confirmed 21: 1 critical, 3 high, 3 medium, 8 low and 6 informational.
    • The critical one was an internal payment endpoint that could be reached through the public gateway.
    • Fixes: the route was removed, the payment service now requires its own service token, and the amount captured is reconciled against the amount owed.
    • A review of the fixes found seven more defects. They were fixed with tests that fail on the old code.

Limitations

  • Never in production. The project had no public users, and Stripe ran in test mode only. The Google Wallet issuer is still in demo mode, and Apple Wallet passes were tested only with a test certificate.
  • Built for one operator. Pieces for serving several operators exist but are switched off by design.
  • Deferred to a real launch:
    • mutual TLS between services
    • alerts routed to a pager
    • exported traces
    • point-in-time database recovery
    • a registered SMS sender ID
  • Not yet deployed. The per-service outbox fix was verified locally but has not yet been deployed to the cloud demo.
Map of Enniscorthy with a pin for each of the 36 parking locations
Find parking on the map
Grafana dashboard with request rate, error rate and request duration for each backend service
Grafana dashboard for the backend services on the local stack
Booking form with vehicle registration, receipt email, duration and the total to pay
Booking a session from a QR sign. No app or account needed.

Contact

Have a project in mind?

Send a short email with what you want to build, your timeline and any useful links.

flaviu.vanca@sketysoft.com