Requirements
Functional Requirements:
Core
- Vehicle entry
- At entry gate: detect vehicle, identify type (car/motorbike/truck/EV), assign a spot (or deny if full).
- Issue a ticket (QR / barcode / license-plate-linked) with entry timestamp.
- Vehicle exit
- At exit gate: scan ticket / plate, compute fee, accept payment, open barrier.
- Close ticket and free the spot.
- Spot allocation
- Support spot types:
COMPACT,LARGE,MOTORBIKE,EV_CHARGING,DISABLED. - Allocation rules (configurable): closest-to-entry, floor preference, EV only, etc.
- Support spot types:
- Real-time availability
- Query available spots by lot/floor/spot-type.
- Show counts and optionally “guidance” (zone-based).
- Payments
- Payment methods: cash (optional), card, e-wallet.
- Refund/cancel policy (optional).
- Support grace period and lost-ticket fee policy.
- Admin operations
- Manage lots/floors/spots (add/remove/disable).
- Manage pricing rules and promotions.
- Force open/close gates, mark spot as blocked/maintenance.
- Enforcement & audit
- Keep history: tickets, payments, spot changes.
- Dispute handling (manual override).
Nice-to-have (common in real deployments)
- License Plate Recognition (LPR) to speed entry/exit.
- Reservations (for premium members).
- Multi-lot portfolio management.
Non-Functional Requirements:
- Availability: gates must work even if cloud is down.
- Target: 99.9%+ overall; gate control should degrade gracefully.
- Latency: entry/exit should be fast.
- Target: ticket issuance + allocation < 300ms p95 (excluding hardware scan).
- Consistency: no double-allocation of a spot.
- Strong consistency for “spot occupancy state”.
- Scalability: handle peak bursts (e.g., stadium).
- Many gates → high contention on allocation.
- Reliability & Recovery
- Handle lost connectivity: local cache + sync.
- Idempotent APIs to avoid double charges.
- Security
- RBAC for admin, audit logs.
- PCI scope minimized: payment tokenization via PSP.
- Observability
- Metrics: gate latency, allocation failures, payment failures, occupancy accuracy.
- Maintainability
- Pricing rules configurable (not hard-coded).
- Data privacy
- Plate numbers are PII in many regions; encrypt at rest; retention policy.
API Design
Key domain objects
ParkingLot(lotId, name, address, timezone)Floor(floorId, lotId, level)Spot(spotId, floorId, type, status: AVAILABLE|OCCUPIED|OUT_OF_SERVICE|RESERVED, zone)Ticket(ticketId, lotId, vehicle: {plate?, type}, entryGateId, entryAt, assignedSpotId, status: ACTIVE|CLOSED|LOST)Payment(paymentId, ticketId, amount, currency, method, status, providerRef)PricingRule(ruleId, lotId, version, definition)
Public-facing APIs (for gate devices + apps)
Entry flow
POST /v1/tickets/entry
{
"lotId": "L1",
"gateId": "GATE_E_01",
"vehicle": { "type": "CAR", "plate": "SGP1234A" },
"preferences": { "needEvCharging": false, "needDisabled": false }
}
Response:
{
"ticketId": "T_9f...",
"issuedAt": "2026-02-24T10:01:22+08:00",
"spot": { "spotId": "S_2_041", "floorId": "F2", "zone": "B" },
"barcode": "....",
"status": "ACTIVE"
}
Exit pricing quote (optional but practical)
GET /v1/tickets/{ticketId}/quote
Response:
{
"ticketId": "T_9f...",
"entryAt": "...",
"exitAt": "...",
"amount": 6.50,
"currency": "SGD",
"breakdown": [{"rule":"weekday_hourly","amount":6.50}]
}
Pay
POST /v1/payments
{
"ticketId": "T_9f...",
"method": "CARD",
"paymentToken": "tok_xxx",
"idempotencyKey": "uuid-unique"
}
Exit confirmation / close ticket
POST /v1/tickets/{ticketId}/exit
{
"gateId": "GATE_X_02",
"paidPaymentId": "P_12...",
"vehiclePlate": "SGP1234A"
}
Availability
GET /v1/lots/{lotId}/availability?spotType=COMPACT
Response:
{ "lotId":"L1", "counts": { "COMPACT": 120, "EV_CHARGING": 8 } }
Admin APIs
- PUT
/v1/spots/{spotId}(status, type) - POST
/v1/pricingRules/publish(new version) - GET
/v1/reports/occupancy?from=...&to=...
API correctness rules (important)
- All state-changing endpoints support Idempotency-Key.
- Gate endpoints must tolerate retries without double-allocations or double-charges.
- Use server time (lot timezone) for billing.
High-Level Design
Architecture style
This is a classic edge + central system:
- Edge (on-prem at parking lot) for low latency + offline:
- Gate Controller Service (local)
- Local cache / local DB (small)
- Central (cloud) for portfolio mgmt, pricing, reporting:
- Ticketing, Allocation, Pricing, Payment Orchestration, Admin, Analytics
3.2 Services (logical)
- Gate Service (Edge)
- Talks to barrier, scanner, LPR camera.
- Calls central APIs; falls back to local logic if offline.
- Ticket Service
- Creates/updates tickets, lifecycle.
- Spot Inventory Service
- Source of truth for spot status, occupancy counts.
- Allocation Service
- Chooses a spot (policy-based). Must be consistent + low latency.
- Pricing Service
- Computes fee based on rules + ticket timeline.
- Payment Service
- Integrates PSP, manages idempotency, payment states.
- Admin/Config Service
- Lots, floors, spots config; pricing publish; user roles.
- Event Bus / Stream
TicketCreated,SpotOccupied,PaymentCompleted,TicketClosed- Feeds analytics, monitoring, reconciliation.
Data stores
- Spot Inventory DB (needs strong consistency)
- Postgres with row-level locks OR Redis + Lua + persistence OR specialized allocator store.
- Ticket DB
- Postgres (transactional), partition by lotId/time.
- Event store/stream
- Kafka / Pulsar (optional), or cloud pub/sub.
Key flows (HLD)
Entry
- Gate scans plate/type → calls
Ticket Service. - Ticket Service calls Allocation Service.
- Allocation Service reserves spot via Inventory Service (atomic).
- Ticket created with assigned spot.
- Gate prints/returns QR; barrier opens.
Exit
- Gate scans ticket/plate → Ticket Service fetches ticket.
- Pricing Service computes amount.
- Payment Service processes payment (idempotent).
- Ticket Service closes ticket + Inventory frees spot.
- Barrier opens.
Detailed Component Design
Spot Inventory + Allocation (the hard part)
Core invariants
- A spot can be OCCUPIED by at most one ACTIVE ticket.
- Ticket closure must free exactly one spot.
- Allocation must avoid “overselling” during concurrency spikes.
Approach A (recommended): DB-backed allocation with atomic reservation
Schema (simplified)
spots(spot_id, lot_id, floor_id, type, status, zone, updated_at)spot_reservations(spot_id, ticket_id, reserved_at, expires_at, state)tickets(ticket_id, lot_id, spot_id, status, entry_at, exit_at, vehicle_plate_hash, ...)
Algorithm
- Pick candidate spots by policy (zone/floor/type).
- Reserve with atomic update:
UPDATE spots SET status='OCCUPIED' WHERE spot_id=? AND status='AVAILABLE'- If affected rows = 1 → success.
- If fail → try next candidate.
Trade-offs
- ✅ Strong correctness, easy audit
- ✅ Works with Postgres + indexes
- ❌ Contention if you scan too wide; mitigate via zone counters + shortlist
Improve performance: Zone-based counters + shortlist
Maintain zone_availability(lot_id, zone, type, available_count) updated transactionally.
Allocation:
- Choose best zone with available_count > 0.
- Query only that zone for a spot.
- Reserve with atomic update.
This reduces lock contention.
Expiring reservations
If you want “soft reservation” (issue ticket but car hasn’t reached spot), use RESERVED state with TTL; if not occupied within N minutes, release.
Ticket Service design
State machine
ACTIVE→ (CLOSED|LOST)- Close requires: payment satisfied OR policy allows pay-on-exit cash.
Idempotency
POST /tickets/entrywithidempotencyKeyfrom gate.- Store request hash to return same ticket on retry.
Plate handling
- Store plate hashed/encrypted. Keep raw plate only if absolutely necessary and compliant.
Pricing Service (rules engine-lite)
Why: pricing changes constantly; don’t hardcode.
Rule model
- Versioned JSON rules:
- time bands, day types, free minutes, max daily cap, overnight, EV surcharge, holidays.
- Evaluate:
- Build timeline from entryAt → exitAt
- Apply rules in order (priority)
- Return breakdown for transparency
Trade-offs
- ✅ Flexible
- ❌ Complexity. Keep it small: don’t build a full DSL unless needed.
Payment Service
Key concerns
- Idempotency to avoid double charge:
- Use
idempotencyKeyper ticket payment attempt.
- Use
- Payment states:
INITIATED -> AUTHORIZED -> CAPTURED/FAILED -> REFUNDED
- Integrate PSP via tokenization (minimize PCI).
Exit gate behavior
- Gate should not depend on payment callback.
- Use synchronous capture if possible; if async, allow “pending exit” policy.
Edge Gate Service (offline-first)
Responsibilities
- Device integration: barrier, printer, scanner, LPR.
- Local fallback:
- Cache last known availability + pricing rules snapshot.
- If cloud down: issue offline tickets with local sequence prefix (lotId + timestamp + counter).
- Sync when connection restored.
Conflict handling
- Offline allocation cannot guarantee global correctness.
- Mitigation:
- Use local inventory partition per gate/zone (pre-allocated quota) so offline doesn’t oversell.
- Or operate in degraded mode: “allow entry but no guaranteed spot” (depends on business).
Observability & audit
Metrics
- Allocation success rate, retries per allocation
- Ticket create latency p95/p99
- Payment failure rate
- Occupancy accuracy (sensor vs DB)
- Offline mode duration
Logs
- Correlate by
ticketId,gateId,requestId
Events
- Emit domain events for analytics and reconciliation.