Published on

Building Modern Marketplace Platforms End-to-End: Frontend, Backend, Security, Retries, and Cloud Deployment

Authors
  • avatar
    Name
    Motions Technologies
    Twitter

Building Modern Marketplace Platforms End-to-End

Food delivery and multi-merchant marketplaces look deceptively simple: browse a store, pay, cook, assign a driver, deliver. In production they are distributed systems with money, identity, geo, and real-time state—and every hop can fail.

This article walks through how we build platforms in that class (exemplified by UmaMeats): product surfaces, service boundaries, frontend and backend stacks, security, retry and resilience, event architecture, and how we deploy to the cloud without burning the budget. The patterns are portable to grocery, courier, and multi-sided SaaS marketplaces.


Table of contents

  1. What you are actually building
  2. Reference architecture
  3. Frontend: web, mobile, and headless clients
  4. Backend: bounded microservices
  5. Data plane: DynamoDB, Redis, Kafka
  6. The happy path as a state machine
  7. Payments as a modular subsystem
  8. Delivery orchestration and GPS
  9. Retry mechanisms that earn their keep
  10. Security: defense in depth
  11. Deploying to the cloud
  12. Cost-aware infrastructure defaults
  13. Observability and ops runbooks
  14. A professional delivery checklist
  15. Closing

1. What you are actually building

A marketplace like UmaMeats is not one app. It is a product system:

SurfaceAudienceTypical host
Marketing / restaurant SaaS dashboardMerchants, opswww. + Vercel
Customer web appDiners / shopperscustomer. subdomain
Driver web appCouriersdriver. subdomain
Customer + driver mobileSame roles, native UXExpo / React Native, TestFlight & Play
Public API edgeAll clientsapi. behind a shared ALB
Async backboneServices onlyKafka + Redis

Professionally, you treat those surfaces as separate deployables that share APIs, design tokens, and domain language—not one monorepo UI that grows forever.

Capabilities that force architecture

  • Multi-tenant stores with menus, hours, and fulfillment modes (kitchen vs proxy/grocery)
  • Payments with capture, refunds, connected accounts / payouts
  • Order lifecycle that must stay consistent when Kafka or a consumer hiccups
  • Driver presence and soft offers with GPS freshness
  • Identity for drivers (documents + biometric verification)
  • Optional POS transmission with vendor adapters and retries
  • Auth across Google OAuth, credentials, email verification, and 2FA

If any of those live “inside the Next.js route handler,” you will eventually rewrite them under load.


2. Reference architecture

System context

Loading diagram…

Design principles we enforce

  1. Clients are thin. Matching, fees, and payment confirmation live in APIs.
  2. One concern per service. Order lifecycle ≠ payment capture ≠ driver marketplace.
  3. Events for cross-service side effects. HTTP for queries and commands; Kafka for fan-out.
  4. Idempotency is mandatory on money and assignment paths.
  5. Cost defaults are intentional. On-demand DynamoDB, shared ALB, Fargate sized to fit, no NAT for demos.

3. Frontend: web, mobile, and headless clients

Web stack

We ship Next.js apps (App Router) with:

  • NextAuth for session management (credentials + Google)
  • Strong typing against shared API contracts
  • i18n (at least English + French for Canadian markets)
  • Role-specific UX: customer checkout, driver online toggle, merchant kitchen board

Custom subdomains are not cosmetic. They clarify cookies, OAuth callback URLs, and CORS allow-lists:

  • Merchant / SaaS → apex or www
  • Customer → customer.
  • Driver → driver.
  • API → api. (ALB + TLS)

When domains change, every microservice CORS config and every OAuth redirect URI must move together. Treat domain migration as a security change, not a DNS ticket.

Mobile parity

Mobile (Expo / React Native) should reuse:

  • The same API base URL and auth model
  • The same status enums (PENDING_PAYMENT, CREATED, PREPARING, READY_FOR_PICKUP, …)
  • Maps for in-app navigation on the driver side
  • Sentry (or equivalent) for crash and release health

Ship TestFlight / Play with a cookbook: reviewer accounts, help URLs that actually 200, and no hardcoded staging hosts in production builds.

Frontend responsibilities (and non-responsibilities)

Do on the clientDo not on the client
Optimistic UI for cart editsFinal payment capture truth
Show Active orders from CREATED+Hide PENDING_PAYMENT as “active” if product says otherwise
Poll / SSE for tracking UXImplement Haversine matching
Collect tip and addressMint Stripe secrets

4. Backend: bounded microservices

A practical service map for a UmaMeats-class platform:

ServiceOwns
customer-apiCustomer identity, profiles, addresses
user-apiMerchant users and dashboard auth surfaces
store-apiStores, hours, fulfillment mode
menu-item-apiCatalog and checkout line items
order-apiOrder aggregate, kitchen transitions, payment event consumption
payment-apiStripe intents, webhooks, transactions, payouts hooks
driver-apiDriver profile, marketplace accept, earnings views, identity gating
events-apiHTTP → Kafka bridge for delivery lifecycle
delivery-orchestration-apiPresence, soft offers, nearby queries, tracking
pos-integration-apiVendor adapters, transmit + webhook + retry
reviews-api / user-content-apiRatings and media

Thin controllers, fat services

Controllers validate auth and shape HTTP. Services own business rules and throw domain errors the API layer maps to status codes. This keeps Spring Boot (or any framework) from becoming a junk drawer of @RestController logic.

Shared libraries worth extracting

  • Messaging serializers, error handlers, DLT wiring, outbox helpers
  • Auth / JWT validation filters
  • Structured logging with redaction
  • Trace id propagation (HTTP headers → MDC → Kafka headers)

Centralizing “how we talk to Kafka” prevents twelve slightly wrong retry stories.


5. Data plane: DynamoDB, Redis, Kafka

DynamoDB (system of record)

Use on-demand (PAY_PER_REQUEST) tables for early and mid scale. Typical domain tables:

  • customers, users, stores, menu items, orders, transactions
  • drivers, reviews, payout methods
  • 2FA secrets, verification tokens
  • POS config / sync logs

Messaging tables (when outbox + idempotency are enabled):

TablePurpose
event-outboxDurable publish queue (PENDINGPUBLISHED) with nextAttemptAt
processed-eventsConsumer idempotency keys {consumerGroup}#{eventId} + TTL

Access patterns need GSIs early (for example status indexes for marketplace queries). Design keys around how you query, not how slides look.

Redis (ephemeral presence)

Driver GPS is a hot, loss-tolerant dataset:

  • GEOADD / GEOSEARCH for nearby drivers
  • Short TTL so stale couriers disappear
  • Update interval on the order of ~10 seconds while online

Lock Redis behind security groups so only the ECS task SG can reach port 6379. Never expose Redis to the public internet “for convenience.”

Kafka (async backbone)

Critical topic families:

  • payment.events — e.g. PAYMENT_SUCCESS
  • order.events — e.g. ORDER_PAID
  • delivery.events / status / eta
  • Soft-offer / assignment topics
  • Matching *.DLT siblings for every consumed topic

Partitions (commonly 3 at early scale) give parallel consumers without over-sharding.

Loading diagram…

6. The happy path as a state machine

Professional platforms document the canonical flow and refuse to invent ad-hoc status strings in each app.

Customer pay → kitchen → driver → deliver

Loading diagram…

Product rules that prevent support tickets

  • PENDING_PAYMENT is not Active for the customer “Active orders” tab—by design.
  • Kitchen modes wait for READY_FOR_PICKUP + UNASSIGNED before marketplace visibility.
  • Proxy / grocery modes may dispatch at CREATED (no kitchen step).
  • Soft-offer windows and radius filters can briefly hide offers; ops UIs must show why.

When Stripe shows COMPLETED but the order stuck in PENDING_PAYMENT, you do not “fix the UI”—you replay PAYMENT_SUCCESS (or drain the outbox) so the consumer runs the full side-effect path.


7. Payments as a modular subsystem

Payment code is where vendor lock-in and security bugs concentrate. We structure it as:

PaymentApiController
PaymentFacadeService  (transactions, refunds, webhooks, Kafka)
PaymentProcessorFactory
   Stripe | PayPal | Square | Adyen adapters
PaymentProcessor / PayoutProcessor / WebhookHandler interfaces

Why the facade + adapters matter

  • Controllers stay stable while processors evolve
  • Tests inject mocks without hitting Stripe
  • Multi-country or multi-acquirer strategies become config, not forks
  • Webhook signature verification stays next to the adapter that understands it

Hard rules

  1. Never trust the client for payment success—webhooks (or server-side confirm) are source of truth.
  2. Persist a transaction record before/with event publish.
  3. Prefer outbox when enabling durable messaging so Dynamo commit and Kafka produce cannot diverge silently.
  4. Store Stripe secrets in Secrets Manager, not env files in git or mobile binaries.

8. Delivery orchestration and GPS

Matching is a state machine with a map, not a CRUD list.

Presence

  • Drivers ping location while online
  • Redis GEO for radius queries
  • Durable driver attributes (docs, region, rating) stay in DynamoDB

Soft offers

  1. Query nearby eligible drivers
  2. Score (distance + simple business weights)
  3. Offer with timeout
  4. On decline/timeout → next candidate
  5. On accept → emit assignment; mark order assigned

Separation of concerns

LayerResponsibility
order-apiLifecycle truth
driver-apiAccept UX + driver records
delivery-orchestrationPresence + offer algorithm
events-apiHTTP gateway into Kafka topics

Clients never embed matching loops. That keeps App Store builds and web apps from disagreeing on radius.


9. Retry mechanisms that earn their keep

Retries without taxonomy create duplicate charges, duplicate offers, and silent loss. Professionals classify failures first.

Failure classes

ClassExampleStrategy
Transient429, 503, network blipExponential backoff + jitter
PoisonInvalid JSON, schema breakNo retry → DLT immediately
Business rejectionCard declined, ineligible driverNo retry; user-visible outcome
Dual-write gapDB committed, Kafka produce failedOutbox + publisher retries
Idempotent replaySame PAYMENT_SUCCESS redeliveredProcessed-events + status guards

HTTP / SDK retries

attempt 1 → wait 200ms
attempt 2 → wait 400ms
attempt 3 → wait 800ms
attempt 4 → wait 1600ms (+ jitter)
give up → dead letter / alert / manual replay

Apply retries to idempotent or safely-replayable calls. For non-idempotent POSTs, use idempotency keys (Stripe-style) or server-side dedupe.

Kafka consumer stop-loss

  1. Do not catch-and-swallow in listeners while auto-commit is on.
  2. Use a DefaultErrorHandler (or equivalent) with backoff.
  3. Route exhausted failures to .DLT.
  4. Prefer manual ack with clear success boundaries.
  5. Enforce idempotency: eventId + durable processed record + status machine guards.

Outbox publisher

Loading diagram…

Operational footgun: enabling outbox-enabled=true while the publisher is not draining leaves payments completed and orders forever PENDING_PAYMENT. Gate the flag on verified tables + a live publisher.

POS transmission retries

Vendor APIs fail. A POS integration service should:

  • Transform order → vendor payload via adapters
  • Transmit with exponential backoff
  • Log sync attempts (pos-sync-log)
  • Emit transmitted / transmission.failed events
  • Allow ops to replay a single order without re-charging the customer

Client-side retries

Mobile and web should retry reads aggressively and writes carefully:

  • Idempotent GETs: retry with backoff
  • Place order / accept delivery: disable double-submit, show in-flight state, reconcile from server
  • Location sync: drop-and-replace latest point (last write wins) rather than queueing stale GPS

10. Security: defense in depth

Security is not a final sprint checklist. It is part of every boundary above.

Identity and access

  • NextAuth sessions on web; secure token handling on mobile
  • Google OAuth with correct callback URLs per subdomain
  • Email verification for credential accounts
  • TOTP 2FA (and backup codes) for elevated roles (merchants, drivers)
  • Backend endpoints for Google login, verify-email, enable/verify/disable 2FA per audience

Driver identity verification

Before a driver can go online:

  1. Create Stripe Identity session (license + selfie)
  2. Drive UX via Stripe.js / mobile SDK
  3. Process webhooks → NOT_STARTED | PENDING | VERIFIED | FAILED | REQUIRES_INPUT
  4. Gate AVAILABLE status on VERIFIED

Secrets for Stripe stay in AWS Secrets Manager; ECS task roles fetch them—apps never embed restricted keys.

Network and CORS

  • Explicit origin allow-lists per environment (localhost + production subdomains)
  • allowCredentials only when cookies/sessions require it
  • ALB terminates TLS; prefer HTTPS everywhere
  • Redis and Kafka on private IPs; SG rules limited to ECS

Webhook security

  • Verify Stripe signatures
  • Reject unsigned or skewed timestamps
  • Treat webhooks as untrusted input even though they are “from Stripe”

Data protection

  • Encrypt sensitive fields where needed (e.g. backup codes)
  • Short CloudWatch retention in non-prod; redact PII in structured logs
  • Separate demo/reviewer accounts from real PII in shared environments
  • S3 buckets for logos/media with least-privilege IAM on task roles

Application hardening

  • Validate all status transitions server-side
  • Authorize every order/driver/store access by subject, not by “I know the UUID”
  • Rate-limit auth and payment session creation
  • Keep dependency and container image updates in CI

Threat-focused thinking for marketplaces

ThreatMitigation
Replay payment successIdempotent consumers + transaction status
Stolen driver account2FA + Stripe Identity gate
CORS misconfig after domain moveShared checklist + integration test of preflight
Kafka silent lossNo swallow; DLT; lag alerts
Secret leak in mobilePublic keys only; restricted keys server-side

11. Deploying to the cloud

Split deploy model

TierPlatformWhy
Web frontendsVercelCDN, previews, custom domains, fast iteration
MobileExpo EAS / Fastlane + store pipelinesReviewer builds, channel separation
APIsAWS ECS FargateStable JVM services, shared ALB path routing
DataDynamoDB + Redis + KafkaManaged scale for DB; right-sized brokers for events

ECS shape

  • Cluster per product (e.g. umameats-api)
  • One service per microservice
  • Target groups with /actuator/health (or equivalent)
  • ALB path rules, for example:
    • /api/v1/delivery/* → delivery-orchestration
    • /api/v1/pos/* → pos-integration
    • other /api/v1/... prefixes → owning services

CI/CD loop

Loading diagram…

Prefer building on CI (correct JDK, reproducible images) over “works on my laptop” Docker from mismatched local JDKs.

Environment discipline

  • Separate env vars for Vercel projects (NEXTAUTH_URL, API base URLs, OAuth client IDs)
  • ECS task definitions inject Kafka bootstrap, Redis host, table names, feature flags (outbox-enabled)
  • Never promote a frontend pointing at the wrong API host

Domain cutover checklist

  1. Add Vercel domains; confirm HTTP 200 + TLS
  2. Update NEXTAUTH_URL and OAuth console redirect URIs
  3. Update Spring CORS allow-lists across all services
  4. Redeploy APIs; smoke preflight from each subdomain
  5. Update docs and mobile config

12. Cost-aware infrastructure defaults

Marketplace demos often overspend on always-on capacity. Defaults that still look professional:

ChoiceRationale
DynamoDB on-demandTraffic is spiky; provisioned “for later” wastes money
One shared ALBMany target groups beat many idle balancers
Fargate right-sized tasksMany services fit 0.25 vCPU / 0.5 GB; grow with evidence
Desired count 1 for early sandboxesScale critical paths (order, driver, events, delivery) first
Skip NAT for public Fargate demosNAT is a classic silent bill
Short log retention non-prodCloudWatch adds up across 10+ services
Stay on ECS until EKS is justifiedControl plane + ops cost rarely win under mid scale

At a few thousand orders per day, compute is rarely the bottleneck—Kafka memory, Redis HA, and missing auto-scaling usually bite first. Fix those before rewriting the platform on Kubernetes.

Nightly scale-to-zero can be valid for demos; turn it off when revenue needs 24/7 dispatch.


13. Observability and ops runbooks

What to measure

  • Payment → order transition latency (COMPLETEDCREATED)
  • Consumer lag and DLT depth
  • Soft-offer accept rate and time-to-first-offer
  • GEO query latency
  • ECS CPU/memory on order/driver/payment
  • Mobile crash-free sessions

Tracing

Propagate a trace id across:

  1. Browser / mobile request headers
  2. API logs (MDC)
  3. Kafka headers
  4. Downstream consumers

Without this, “payment worked but kitchen never lit up” becomes archaeology.

Runbook snippets worth owning

  • Replay PAYMENT_SUCCESS for stuck paid orders
  • Inspect outbox PENDING rows and publisher health
  • Drain / replay DLT after poison fix
  • Verify Redis GEO membership for an online driver
  • Confirm ECS runningCount == desiredCount for the critical set

Smoke checklist before calling a release “done”

  • Core ECS services healthy
  • Messaging tables active if outbox/idempotency enabled
  • Critical topics + DLTs exist
  • New pay: transaction COMPLETED and order CREATED within seconds
  • Customer Active shows the order
  • Merchant can advance to READY_FOR_PICKUP
  • Driver marketplace includes the order when filters allow

14. A professional delivery checklist

Use this as a program-level scoreboard—not a motivational poster.

Architecture

  • Service boundaries documented with owners
  • Sequence diagram for pay → cook → assign → deliver
  • Status enums shared across web, mobile, and APIs

Resilience

  • Kafka retries + DLT on every money/assignment consumer
  • Idempotency store for critical consumers
  • Outbox strategy decided and verified (or explicitly off)
  • POS and Stripe webhook paths have replay stories

Security

  • OAuth callbacks and CORS match production domains
  • Secrets in Secrets Manager; IAM least privilege
  • Driver identity gate before online
  • 2FA for elevated roles
  • Redis/Kafka not public

Delivery

  • CI builds images; ECS rolling deploys
  • Vercel envs correct per app
  • Health checks and ALB rules verified
  • Cost defaults recorded in the PR / runbook

Product proof

  • E2E path with reviewer accounts
  • Ops can answer “why is this order stuck?” from timeline + metrics

15. Closing

Building a platform like UmaMeats is less about picking fashionable frameworks and more about honoring boundaries under failure:

  • Frontends that sell the experience without owning money or matching
  • Microservices that each protect one invariant
  • DynamoDB for durable truth, Redis for presence, Kafka for fan-out
  • Retries that know the difference between transient, poison, and business outcomes
  • Security woven into auth, webhooks, network, and identity gates
  • Cloud deploy that is boring: Vercel for UI, ECS + shared ALB for APIs, cost-aware data plane

Do that well and “professional” stops being a vibe. It becomes a system you can operate at dinner rush—when retries, DLTs, and clear state machines matter more than any slide deck.

If you are designing a similar marketplace or modernizing a monolith into this shape, start with the payment → order → dispatch spine, make it idempotent and observable, then grow POS, reviews, and mobile polish on a foundation that already survives redelivery.


Written for Motions Technologies — sharing the engineering patterns we use when shipping real multi-sided platforms.