Auction Platform Architecture
Monolith → Event-Driven Microservices
One codebase. One deploy. Everything fails together.
Pickles Asia's auction platform had been built as a single NestJS monolith since its early days. Over 4 years it absorbed every new domain: vehicle listings, live auction streaming, real-time bidding, payment integrations, user authentication, media upload and processing, email/SMS notifications, and management reporting. By 2025, the codebase was over 180,000 lines of TypeScript with no module ownership boundaries.
The most dangerous problem was blast radius. The live auction path - the revenue-critical core of the business - shared a process and database with background batch jobs, report generation, and media processing. A slow query from a nightly report could spike response times for active bidders. A crash in any module brought everything down. Deployments were high-risk events requiring coordination across all teams simultaneously. Scaling any one part meant scaling the entire application.
- Live auction, bidding, payments, notifications, and media all in one NestJS process - full system down if any module crashes
- Shared MySQL database across all domains - one slow report query degraded live auction response times
- All teams deploy together - one team's unreviewed change blocked every other team's release
- No horizontal scaling for individual services - scaling live bidding meant scaling the reporting module too
- No clear module ownership - refactoring any domain risked breaking unrelated functionality
- No event history - inter-module calls were synchronous function calls, making audit trails and replay impossible
| Domain | Isolation | Deploy risk |
| Live Auction Engine | None | Shared |
| Real-time Bidding | None | Shared |
| Payment Integration | None | Shared |
| Media Processing | None | Shared |
| Notifications | None | Shared |
| Reporting | None | Shared |
Domain decomposition → Event contracts → Migration roadmap
Every decision documented with context, trade-offs, and rejected alternatives
Decision: EventBridge as the event bus. Why: Built-in schema registry enforces contract validation at publish time. Content-based filtering lets consumers subscribe to event subsets without custom routing logic. Native CloudWatch integration provides event bus observability with zero additional instrumentation. Trade-off accepted: Higher cost per event vs raw SQS; acceptable at Pickles Asia's event volume (<500k events/day).
Decision: Each service owns its data store; no other service may query it directly. Why: Shared database is coupling at the deepest layer - schema changes break multiple services simultaneously. Database-per-service enables independent scaling, technology choice, and deployment. Trade-off accepted: Cross-domain queries require read model projections maintained via event consumers. Eventual consistency accepted for non-financial data.
Decision: Strangler Fig over Big Bang rewrite. Why: Big Bang migration of a live production system with real-time auction bidding is too high-risk to execute without prolonged parallel running. Strangler Fig allows the monolith to continue operating at full capacity while new services gradually absorb domains. Feature flags gate traffic between old and new implementations at each phase boundary. Trade-off accepted: Temporary complexity from dual code paths during migration phases.
Decision: ElastiCache Redis as the primary store for active auction bid state; all bids written to RDS asynchronously for the audit log. Why: Live auction bidding requires sub-10ms bid acceptance latency. RDS at peak load (>200 concurrent bidders) would not meet this SLA. Redis Sorted Sets provide O(log N) bid ordering. Trade-off accepted: Redis is non-durable by default - mitigated with AOF persistence and replica configuration. RDS audit log provides full recovery source.
3 phases. Sequenced by blast radius risk. Each phase gated independently.
Neither service is on the synchronous hot path of live auction bidding. Notifications are fire-and-forget; media processing is background. Extracting these first validates the EventBridge pipeline, the deployment pattern, the per-service CI/CD approach, and the runbook structure before touching core auction logic. Risk gate: notification delivery success rate ≥99.9%, media processing latency within 10% of baseline.
Both services are predominantly read-heavy with no real-time write path dependency from the live auction. Reporting is extracted to its own RDS read model populated by EventBridge consumers - no more direct joins on the monolith's primary database. Vehicle Catalogue is extracted with a dual-write pattern during cutover. Risk gate: report data consistency audit (±0 discrepancy vs monolith output), catalogue API response parity test suite passing at 100%.
The most complex phase. Auction Engine and Bidding Service share deeply intertwined state transitions. Requires Redis cluster deployment, RDS schema migration, feature flag infrastructure, and a prolonged parallel-run period where both monolith and services process bids simultaneously with output comparison. Canary rollout: 5% → 25% → 50% → 100% with manual approval gates between stages. Full rollback procedure documented with <2-minute RTO. Only proceeds after Phase 1 and Phase 2 are stable in production for ≥30 days.
Architectural choices, tradeoffs, and what nearly broke production
EventBridge + DDD, not gRPC + REST mash
EventBridge enables temporal decoupling - the Payments service completes independently of the Notifications service. Synchronous calls require all downstream services to be available, creating availability blast radius. DDD bounded contexts map cleanly to microservices; EventBridge publishes domain events defined in the ubiquitous language, making contracts explicit and versioned.
Strangler Fig pattern, not big bang
Big bang rewrites hide unknown unknowns until production. Strangler Fig gradually routes requests from the monolith to new microservices. Phase 1: Extract reporting (lowest risk, no revenue impact). Phase 2: Extract media & notifications. Phase 3: Extract live auction (highest risk). Each gate allows us to learn from production feedback before the next phase.
What nearly failed: Event schema versioning
Initially, the design had unversioned event schemas. As soon as we started extracting services, we realized: if Payments publishes an OrderCompleted event and we add a new field later, all consumer services need schema validation. We added schema versioning with backward compatibility guarantees - old events continue to work, new consumers handle missing fields gracefully.
Lesson: Event contracts are as critical as API contracts. Schema versioning prevents the 'distributed monolith' anti-pattern.
Database per service boundaries - strict
The design explicitly forbids cross-service database access. All inter-service communication goes through events or synchronous APIs. This enforces bounded contexts - Payments owns the orders table, Notifications owns the notification log. No hidden coupling via foreign keys. Enforcement happens in infrastructure code review and runbooks.
Cost · Risk · Tradeoffs
Estimated values based on production metrics and monitoring data
Transformative Results: Before vs After
- Monolithic NestJS appSingle point of failure
- Shared database across all domainsDomain coupling
- All teams deploy togetherSlow releases
- No service boundariesScaling constraints
- 8 microservices designedIndependent deployability
- Database-per-serviceDomain isolation
- Independent deploymentsFaster releases
- Clear bounded contextsIndividual scaling
What was used
Need architecture design for a complex migration or new system?
I design systems from bounded context mapping through to phased migration roadmaps - with the trade-offs documented so your team can own the decisions long-term.