DynamoDB Single-Table Design in Practice: Lessons from Dream Car Brasil
A practical look at implementing single-table design with 7 GSIs for a raffle e-commerce platform serving 20,000+ participants, including atomic transactions, payment fallback chains, and keeping infrastructure costs under $30/month.
When I designed the backend for Dream Car Brasil — a raffle e-commerce platform — I chose DynamoDB with a single-table design. The platform now serves 20,000+ participants with total infrastructure costs under $30/month. Here’s what I learned building it.
Why Single-Table Design
The platform has several access patterns that need to be fast: looking up a user’s purchases, listing available raffles, checking ticket availability, processing checkout, computing leaderboards, and sending notifications. In a relational database, these would require multiple joins across several tables. With DynamoDB’s single-table design, every access pattern maps to a single query against either the base table or one of the 7 Global Secondary Indexes (GSIs).
Modeling the Data
The core entities — users, raffles, tickets, orders, and payments — all live in one table. The partition key and sort key are composite, using prefixes to distinguish entity types. For example, a user record might have PK=USER#123 and SK=PROFILE, while their orders would have PK=USER#123 and SK=ORDER#456. This gives us efficient user-scoped queries.
The 7 GSIs serve specific access patterns: listing active raffles by date, looking up orders by payment status, finding tickets by raffle, computing leaderboards by total spend, and more. Each GSI projects only the attributes needed for its access pattern, keeping costs down.
Atomic Number Reservation
One of the trickiest parts was ticket number reservation. When a user adds numbers to their cart, those numbers need to be atomically reserved so no one else can claim them. I used DynamoDB transactions (TransactWriteItems) to atomically update both the ticket record and the raffle’s available count in a single operation. If any condition check fails — say, the number was already taken — the entire transaction rolls back.
Multi-Provider Payment with Fallback Chains
The platform integrates with multiple PIX payment providers (Asaas, Wisepix) with automatic fallback. If the primary provider returns an error or times out, the system automatically tries the next provider in the chain. Each payment attempt is recorded as a separate item in DynamoDB, creating a full audit trail. Webhook handlers for each provider update the order status, triggering downstream Lambda functions for notifications and ticket confirmation.
Event-Driven Architecture
Eight Lambda functions handle the asynchronous workflows: checkout expiration (using EventBridge Scheduler for per-checkout timers), image processing for raffle banners, leaderboard recomputation, and WhatsApp messages via Wati for purchase confirmations and raffle results. Each function is triggered by DynamoDB Streams or EventBridge events, keeping the system loosely coupled.
Cost Optimization
The entire stack runs under $30/month because of careful design choices: on-demand DynamoDB capacity (you pay per request, not for provisioned throughput), Lambda’s per-invocation pricing, and S3 for image storage. No idle servers, no unused capacity. The monorepo structure (Turborepo) with shared packages also keeps the codebase maintainable as a solo developer.
Key Takeaways
Single-table design requires more upfront modeling effort, but the payoff is massive for query performance and cost. The important thing is to start with your access patterns and work backwards to the table schema — not the other way around. And DynamoDB transactions, while more expensive per operation, are worth it for data integrity in critical flows like payment processing.