Architecture Challenges
Advanced challenges that combine multiple concepts — design complete solutions for complex real-world scenarios.
Objective
The challenges in this section combine multiple architecture concepts into complex scenarios. Unlike the previous labs (which focus on a single topic), here you’ll need to integrate patterns, resilience, security, and operations into complete solutions.
Each challenge has a difficulty level and an estimated time. Try to solve them before looking at the hints.
Challenge 1: Zero-downtime migration
Level: Intermediate | Estimated time: 30-45 minutes
Scenario
Your company has a monolith that handles authentication, catalog, and orders. You need to extract the orders service as a microservice without downtime. The monolith processes 500 orders per hour and cannot be stopped.
Requirements
- Zero downtime during the migration
- The ability to roll back at any point
- Historical order data must be available in the new service
- Existing reports must keep working
Your task
Design a step-by-step migration plan. Consider:
- How do you migrate the data without stopping the monolith?
- How do you gradually redirect traffic?
- How do you verify that the new service works correctly?
- What’s your rollback plan?
Hints
Hint 1: Migration pattern
Use the Strangler Fig pattern:
- Create the new orders service
- Redirect reads first (lower risk)
- Then redirect writes
- Finally, remove the orders code from the monolith
Hint 2: Data migration
Use Change Data Capture (CDC):
- Do an initial dump of the historical orders into the new service
- Set up CDC to replicate changes in real time from the monolith to the new service
- Once both are in sync, redirect the traffic
Hint 3: Verification
Use the Shadow Traffic pattern:
- Send a copy of each request to the new service (without affecting the user)
- Compare the responses from the monolith and the new service
- When the responses match 99.9% of the time, redirect the real traffic
Reference solution
View full solution
Phase 1 — Preparation (week 1-2):
- Create the new orders service with the same API
- Set up CDC from the monolith’s orders table
- Migrate historical data with a batch script
Phase 2 — Verification (week 3):
- Enable shadow traffic: each request to the monolith is duplicated to the new service
- Compare responses automatically and log discrepancies
- Fix bugs until the responses match
Phase 3 — Gradual migration (week 4):
- Redirect 10% of read traffic to the new service
- Monitor latency, errors, and data consistency
- Increase gradually: 25% → 50% → 75% → 100%
Phase 4 — Writes (week 5):
- Redirect writes to the new service
- The new service publishes events that the monolith consumes (for legacy reports)
- Monitor intensively for 48 hours
Phase 5 — Cleanup (week 6):
- Remove the orders code from the monolith
- Disable CDC
- Update reports to use the new data source
Rollback at any phase: Revert the proxy/load balancer configuration to send traffic to the monolith.
Challenge 2: Multi-provider payment system
Level: Advanced | Estimated time: 45-60 minutes
Scenario
Design a payment service that supports multiple providers (Stripe, PayPal, bank transfer) with the following requirements:
- Idempotency: a payment cannot be processed twice
- Resilience: if one provider fails, automatically try another
- Auditing: every payment attempt must be logged
- Reconciliation: the ability to verify that charges match orders
- PCI compliance: don’t store card data in your system
Your task
Design:
- The payment service API
- The internal processing flow
- The data model
- The resilience strategies
- The reconciliation flow
Hints
Hint 1: Idempotency
Use an idempotency key generated by the client:
POST /payments
Headers: Idempotency-Key: uuid-unique-per-payment
Body: { orderId, amount, currency, provider }
Before processing, check whether a payment with that key already exists. If it does, return the previous result without processing again.
Hint 2: Data model
Payment {
id: UUID
idempotencyKey: string (unique)
orderId: string
amount: decimal
currency: string
status: PENDING | PROCESSING | COMPLETED | FAILED | REFUNDED
provider: STRIPE | PAYPAL | BANK_TRANSFER
providerTransactionId: string?
attempts: PaymentAttempt[]
createdAt: timestamp
updatedAt: timestamp
}
PaymentAttempt {
id: UUID
paymentId: UUID
provider: string
status: SUCCESS | FAILED | TIMEOUT
errorCode: string?
errorMessage: string?
responseTime: number
attemptedAt: timestamp
}
Hint 3: Fallback strategy
Define a provider priority order by payment type:
- Credit card: Stripe → PayPal
- Debit card: Stripe → PayPal
- Transfer: bank only
If the primary provider fails with a recoverable error (timeout, 503), try the next one. If it fails with a non-recoverable error (card declined, insufficient funds), don’t retry.
Challenge 3: Design a notification system
Level: Intermediate | Estimated time: 30-45 minutes
Scenario
Design a notification service that supports:
- Channels: email, push, SMS, in-app
- Priorities: critical (immediate), high (< 1 min), normal (< 5 min), low (daily batch)
- Preferences: users can choose which channels to receive by notification type
- Rate limiting: don’t send more than 10 notifications per hour per user
- Templates: notifications use templates with dynamic variables
Your task
- Define the API for sending notifications
- Design the internal processing flow
- Define how you handle user preferences
- Design the rate limiting strategy
- Define what happens when a channel fails
Considerations
- Do you use one queue per priority or a single queue with priorities?
- How do you handle the daily batch of low-priority notifications?
- What happens if the email service is down but push is working?
- How do you avoid sending the same notification twice?
View suggested architecture
[Notifications API]
│
▼
[Priority queue]
├── CRITICAL queue (immediate processing, dedicated workers)
├── HIGH queue (processing < 1 min)
├── NORMAL queue (processing < 5 min)
└── LOW queue (accumulation for batch)
│
▼
[Notification Processor]
│
├── Check user preferences
├── Apply rate limiting
├── Resolve template with variables
│
▼
[Channel Router]
├──► [Email Provider] (SendGrid/SES)
├──► [Push Provider] (Firebase/APNs)
├──► [SMS Provider] (Twilio)
└──► [In-App Store] (WebSocket/DB)
Rate limiting: Use Redis with a per-user counter with a 1-hour TTL. Before sending, check the counter. If it exceeds the limit, enqueue for the next period.
Deduplication: Use a composite key (userId + notificationType + entityId) with a TTL to avoid duplicates.
Challenge 4: End-to-end observability
Level: Advanced | Estimated time: 45-60 minutes
Scenario
You have 8 microservices in production. A user reports that “checkout is sometimes slow.” You can’t reproduce the problem consistently. Design an observability strategy that lets you diagnose this type of problem.
Your task
- What metrics would you collect from each service?
- How would you implement distributed tracing?
- What alerts would you configure?
- How would you correlate logs across services?
- What dashboard would you create for the on-call team?
Success criteria
Your design should make it possible to:
- Identify which service causes the latency in under 5 minutes
- See the complete trace of a user’s request
- Detect degradations before users report them
- Understand the impact of a deploy on the system’s latency
View suggested strategy
Metrics (per service):
- Latency: p50, p90, p95, p99 per endpoint
- Error rate: 4xx and 5xx per endpoint
- Throughput: requests per second
- Saturation: CPU, memory, DB connections, thread pool
Distributed tracing:
- Generate a
traceIdat the API Gateway - Propagate it via the
X-Trace-Idheader to all services - Each service records spans with start, end, and metadata
- Use OpenTelemetry as the standard
Alerts:
- p99 latency > 2x the baseline for more than 5 minutes
- 5xx error rate > 1% for more than 2 minutes
- Message queue growing for more than 10 minutes
- CPU > 80% for more than 5 minutes
On-call dashboard:
- Overview: health of all services (green/yellow/red)
- Flow view: latency per checkout step
- Traces view: the 10 slowest traces from the last hour
- Errors view: errors grouped by type and service
Reflection
After completing the challenges:
- Which challenge was the hardest? Why?
- In which areas do you feel you need more practice?
- Were you able to reuse patterns from previous sections?
- Which challenge most resembles a real problem you face at work?
The challenges are designed to simulate the real complexity of software architecture. There are no perfect solutions — there are well-reasoned solutions.