Decision Simulator
Face real-world scenarios where you must make architectural decisions and weigh their short- and long-term consequences.
Objective
In this lab you’ll face a series of scenarios where you must make architectural decisions under real-world constraints: time, budget, team, and changing requirements. Every decision has consequences that surface in the following rounds.
How it works
- Read the initial scenario along with its constraints
- Choose from the available options (or propose your own)
- Read the consequences of your decision
- Face the next scenario, which follows from your previous choice
There are no universally correct answers — but some decisions are better grounded than others.
Scenario 1: The monolith that keeps growing
Context
Your company has a Node.js monolith that has been in production for 3 years. The team grew from 4 to 12 developers. Deploys take 45 minutes and merge conflicts are constant. The CTO wants to “migrate to microservices.”
Constraints
- Budget: no room to hire more people
- Timeline: 6 months to show results
- The monolith generates $2M/year in revenue — it can’t go down
Options
Option A: Big bang — rewrite everything as microservices
- Risk: high (complete rewrite)
- Estimated time: 8-12 months
- Potential benefit: clean architecture from scratch
Option B: Strangler fig — extract services gradually
- Risk: medium (incremental changes)
- Estimated time: 3-6 months for the first service
- Potential benefit: incremental value, controlled risk
Option C: Modular monolith — reorganize without distributing
- Risk: low (internal refactoring)
- Estimated time: 2-3 months
- Potential benefit: better organization without distributed complexity
Consequences
If you chose Option A
At month 4, the team is 30% of the way through. The monolith keeps receiving new features because the business can’t wait. Now you have two systems to maintain and the team is split. The CTO starts asking when it will be ready.
Lesson: Complete rewrites almost never finish on time. The business doesn’t stop while you rewrite.
If you chose Option B
At month 3, you extracted the notifications service. The monolith’s deploys are a bit faster. The team learned about inter-service communication, service discovery, and distributed monitoring. Now you can plan the next extraction with more confidence.
Lesson: The Strangler Fig pattern lets you migrate gradually with controlled risk. Each extraction teaches lessons for the next one.
If you chose Option C
At month 2, the monolith is organized into modules with clear interfaces. Merge conflicts dropped by 60%. Deploys still take 45 minutes, but now you can do partial deploys per module. The team has a solid foundation to extract services in the future if needed.
Lesson: Sometimes the best distributed architecture is the one you don’t need. A well-organized monolith can scale further than you think.
Scenario 2: Choosing the database
Context
You’re designing a new inventory service. It needs to handle:
- 500,000 SKUs with frequent updates
- Complex queries with filters and aggregations
- Change history for auditing
- Reads 10x more frequent than writes
Options
Option A: PostgreSQL
- Relational model, full ACID
- Excellent for complex queries
- Vertical scaling + read replicas
Option B: MongoDB
- Document model, flexible schema
- Native horizontal scaling (sharding)
- Powerful aggregation queries
Option C: PostgreSQL + Redis
- PostgreSQL as the source of truth
- Redis as a read cache
- Higher operational complexity
Consequences
If you chose Option A
PostgreSQL handles the 500K rows and complex queries well. At month 6, reads start to saturate the database. You add a read replica and the problem is solved. You implement the change history with an audit table and triggers.
Lesson: PostgreSQL is a solid choice for most cases. Don’t underestimate how far a well-configured relational database can go.
If you chose Option B
MongoDB works well at first. At month 3, you discover that aggregation queries with multiple filters are slower than expected. The flexible schema caused data inconsistencies because different parts of the team stored documents with slightly different structures.
Lesson: Schema flexibility is a double-edged sword. Without discipline, it creates technical debt. MongoDB shines when the data model is genuinely hierarchical.
If you chose Option C
The combination works very well for reads. But at month 2, you discover cache invalidation bugs: some users see stale data after updates. You implement an event-based invalidation system and the problem is solved, but operational complexity increased significantly.
Lesson: Adding a cache solves performance problems but introduces consistency problems. Only add a cache when you truly need it, not “just in case.”
Scenario 3: The external service that fails
Context
Your payments service depends on an external provider (Stripe). Over the last few weeks, Stripe has had 3 availability incidents that affected your users. The product team wants payments to “never fail.”
Options
Option A: Add a second provider (PayPal) as a fallback
- You duplicate the payment integration
- If Stripe fails, redirect to PayPal automatically
- Higher complexity in reconciliation
Option B: Implement a retry queue
- Failed payments go to a queue
- They’re retried automatically with exponential backoff
- The user is notified when the payment goes through
Option C: Circuit breaker + graceful degradation
- Detect Stripe failures quickly
- Show the user a clear message and offer to retry later
- Save the attempt to process it once Stripe recovers
Consequences
If you chose Option A
It works, but reconciliation across two providers is complex. Some payments are processed on Stripe and others on PayPal, which complicates financial reporting. On top of that, PayPal has its own failure rate — it’s not immune. At month 3, you have a more resilient system but one that’s significantly more complex to operate.
Lesson: Multiple providers increase availability but multiply complexity. Weigh whether the operational cost justifies the improvement in availability.
If you chose Option B
Automatic retries resolve 90% of transient failures. But you discover a problem: some payments are processed twice because the retry arrives when Stripe had already processed the first one (but responded with a timeout). You implement idempotency with unique keys and the problem is solved.
Lesson: Retries without idempotency are dangerous. Always implement idempotency before implementing automatic retries.
If you chose Option C
The circuit breaker detects failures within seconds and stops sending requests to Stripe. Users see a clear message: “The payment system is temporarily unavailable. Your cart is saved — you can complete the purchase later.” The experience isn’t perfect, but it’s honest and doesn’t create frustration.
Lesson: Sometimes the best response to a failure is to be transparent with the user. Not every problem requires a complex technical solution.
Final reflection
After completing the scenarios, reflect:
- Did you tend to choose the most complex option or the simplest one?
- Did you consider the constraints (time, team, budget) or only the technical requirements?
- Would you change any decision knowing the consequences?
- What pattern do you see in your decisions? Are you conservative or a risk-taker?
The best architectural decisions aren’t the most sophisticated ones — they’re the ones that best fit the real context of the project.