Event marketplace architecture: booking food trucks at scale
TL;DR
- An event marketplace architecture is mostly about modeling availability, search, and workflow state, not pages and forms.
- Use a modular monolith first: Catalog, Search, Availability, Leads/Bookings, Messaging, Payments (optional).
- Prefer a transactional core (PostgreSQL) plus an async event bus for side effects (emails, notifications, indexing).
- Make availability a first-class domain object with idempotent holds and timeboxed expirations.
The problem: “Find me a truck for a date” is harder than it looks
RoFoodTrucks (rofoodtrucks.ro) connects event organizers with verified food trucks across Romania. From an engineering standpoint, the platform’s complexity shows up in three places:
- Time: A food truck can’t be in Cluj and București at the same time, and prep/drive time matters.
- Discovery: Users search by city, cuisine, concept, budget, and capacity—often with fuzzy preferences.
- Trust and workflow: “Request a quote” can turn into negotiation, menu tweaks, deposits, and cancellations.
This is where event marketplace architecture earns its keep: you need domain boundaries that let you ship features without turning the codebase into a ball of mud.
Solution overview: a modular monolith with clear domain seams
If you’re early-stage, start with a modular monolith (single deployable) but enforce boundaries like you would in microservices. It’s the most pragmatic event marketplace architecture for a marketplace that’s still iterating.
Suggested modules
- Catalog: truck profile, menu, photos, service area, tags (BBQ, tacos, vegan)
- Search: query parsing, ranking, facets, geospatial filtering
- Availability: schedules, blackouts, travel buffers, capacity constraints
- Leads/Bookings: request, quote, accept/decline, status transitions
- Messaging: thread per lead, attachments, audit trail
- Verification & Trust: “verified” flags, document checks, reviews/ratings
- Notifications: email/SMS/push (async)
This decomposition is the “spine” of your event marketplace architecture. It also sets you up for future service extraction if needed.
Data model: relational core + event stream for side effects
For marketplace workflows, PostgreSQL is hard to beat. You want constraints, transactions, and good indexing.
A minimal relational model:
food_trucks(id, name, home_city, verified, cuisines[], min_guests, max_guests, price_band, created_at)events(id, organizer_id, city, venue_lat, venue_lng, start_at, end_at, guest_count)leads(id, event_id, food_truck_id, status, created_at)quotes(id, lead_id, price_total, menu_json, terms_json, expires_at)-
availability_blocks(id, food_truck_id, start_at, end_at, kind)wherekind ∈ {booking, blackout, buffer} messages(id, lead_id, sender_id, body, created_at)
Why not store availability as a boolean per day?
Because you’ll need:
- partial-day overlaps
- multi-day festivals
- travel/prep buffers
- temporary holds while a quote is pending
That’s core event marketplace architecture thinking: model the real constraint, not the UI.
Availability engine: overlaps, holds, and idempotency
The availability check must be deterministic and safe under concurrency.
Postgres overlap query
Use range logic (or tsrange) to detect conflicts. Here’s a simple overlap query:
-- Find conflicts for a truck in a candidate time window
SELECT 1
FROM availability_blocks
WHERE food_truck_id = $1
AND start_at < $3
AND end_at > $2
LIMIT 1;
If this returns a row, the truck isn’t available.
Timeboxed holds (soft locks)
When an organizer requests a quote, you often want to “hold” the slot for a short time to prevent double-booking during negotiation.
INSERT INTO availability_blocks (food_truck_id, start_at, end_at, kind)
VALUES ($1, $2, $3, 'buffer')
RETURNING id;
Then attach an expiration policy (e.g., 30–120 minutes) and a cleanup job.
Idempotent APIs
Clients retry. Webhooks retry. Make write operations idempotent:
POST /api/leads
Idempotency-Key: 6f3c0d...
{ "eventId": "...", "foodTruckId": "..." }
Store (idempotency_key, response_hash, created_at) per user and reuse the original response on retries. Strong idempotency is a quiet but critical part of event marketplace architecture.
Search architecture: fast filters, sane ranking
Search is where marketplaces win or lose. If the catalog is small, Postgres full-text plus trigram indexes can carry you far.
Postgres trigram for name/cuisine matching
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX food_trucks_name_trgm
ON food_trucks USING gin (name gin_trgm_ops);
For larger scale, add Elasticsearch/OpenSearch later. Keep the search module behind an interface so you can swap implementations.
Ranking signals worth implementing early
- verified trucks rank higher
- closer distance (venue → home_city centroid or service area)
- response time (median time to reply)
- successful booking rate
Those aren’t “nice-to-haves”; they’re part of the event marketplace architecture because they influence conversion and operational load.
Workflow: state machine for leads and bookings
Model your lead lifecycle explicitly. Avoid sprinkling if (status === ...) across handlers.
A minimal state machine
-
new→contacted -
contacted→quoted -
quoted→accepted|expired|declined -
accepted→booked -
booked→completed|cancelled
Represent transitions in code:
const transitions: Record<string, string[]> = {
new: ["contacted"],
contacted: ["quoted"],
quoted: ["accepted", "expired", "declined"],
accepted: ["booked"],
booked: ["completed", "cancelled"],
};
export function canTransition(from: string, to: string) {
return transitions[from]?.includes(to) ?? false;
}
This is a foundational pattern for event marketplace architecture: predictable state transitions simplify analytics, support, and incident response.
Async side effects: outbox pattern for reliability
Whenever you create a lead or quote, you’ll trigger side effects: send emails, notify the truck, update search indexes.
Do not do these inside the request transaction with “fire and forget.” Use the outbox pattern:
-- In the same DB transaction as lead creation:
INSERT INTO outbox (event_type, payload_json, created_at)
VALUES ('LeadCreated', $1, now());
A worker polls outbox, publishes to a queue, then marks rows as processed.
This pattern is well documented in Martin Fowler’s write-up on the Outbox pattern. It’s one of the most practical building blocks in event marketplace architecture.
Security and trust: verified profiles and auditability
A marketplace needs audit logs. If an organizer disputes a cancellation or a truck disputes a payment, you need immutable facts.
Implement:
- append-only message log per lead
- status change history (
lead_status_events) - admin actions log (who verified what, when)
For identity, follow OWASP guidance for session management and auth flows: OWASP Application Security Verification Standard.
What I learned (and gotchas)
Availability is a product feature disguised as backend logic
If you ignore buffers, you’ll get “available” results that are operationally impossible. Bake in:
- setup/teardown duration per truck
- travel buffer based on distance buckets
- optional “service area” polygons later
Concurrency bugs show up as double-bookings
If two organizers request the same slot at the same moment, you need deterministic conflict checks.
Practical tactics:
- transaction isolation + conflict query
- unique constraints where possible (hard with time ranges)
- short-lived holds + clear UX when a hold expires
Don’t over-microservice too early
A modular monolith is still “real architecture” if boundaries are enforced. In event marketplace architecture, the data model and workflow correctness matter more than deployment topology.
Implementation checklist you can copy
- [ ] Define modules and interfaces (Catalog, Availability, Leads)
- [ ] Add
availability_blockswith overlap checks - [ ] Implement idempotency keys for lead creation
- [ ] Add a lead state machine with transition validation
- [ ] Add outbox table + worker for notifications
- [ ] Add ranking signals and basic search indexes
- [ ] Add audit logs for status changes and admin verification
Helpful next step
If you’re building something like RoFoodTrucks, sketch your lead lifecycle and availability rules on paper first, then encode them as a state machine + overlap constraints. If you want, share your current workflow (statuses + timing rules) in a comment, and I’ll suggest a minimal schema and transition map that fits your constraints.
Originally about event marketplace architecture.
Top comments (0)