← Back to Design Thinking

Design Thinking

Architecture Thinking: Decomposing Problems into Components

Learn how to break down vague product problems into API flows, storage layers, caching layers, queue pipelines, and background workers. This skill differentiates junior engineers from senior ones.

Intermediate20 min read

Architecture thinking is about decomposing problems into API flows, storage layers, caching layers, queue pipelines, and background workers. This skill differentiates a junior engineer from a senior one.


What Is Architecture Thinking?

Architecture thinking is the ability to:

  1. Break down vague problems into concrete components
  2. Convert requirements into system components
  3. Think with flows instead of code
  4. Identify bottlenecks early
  5. Design for scale from day 1
  6. Choose between synchronous and asynchronous flows

A junior engineer might think: "I'll write a function that does X." A senior engineer thinks: "I'll design a system with components A, B, C that interact via flows X, Y, Z."


Breaking Down Vague Product Problems

Example: "Build a Chat System"

Junior Engineer Approach:

  • "I'll create a ChatService class with sendMessage() and receiveMessage() methods."

Senior Engineer Approach:

  • "I need to break this into components:
    1. API Layer: REST API for sending/receiving messages
    2. Message Storage: Database to store messages
    3. Real-time Delivery: WebSocket server for real-time updates
    4. Presence Service: Track online/offline users
    5. Notification Service: Push notifications for offline users
    6. Load Balancer: Distribute WebSocket connections"

The Process

  1. Identify core entities: Users, Messages, Conversations
  2. Identify operations: Send message, receive message, see online users
  3. Identify components: API, Storage, Real-time, Presence, Notifications
  4. Identify flows: How data moves between components
  5. Identify bottlenecks: Where will the system break at scale?

Converting Requirements → Components

Step-by-Step Process

  1. List functional requirements: What must the system do?
  2. Identify data entities: What data do we need to store?
  3. Identify operations: What operations do we need to support?
  4. Map to components: Which components handle which operations?
  5. Design interfaces: How do components communicate?

Example: Design a URL Shortener

Functional Requirements:

  • Create short URL from long URL
  • Redirect short URL to long URL
  • Track click statistics

Data Entities:

  • URL mappings (short URL → long URL)
  • Click statistics (short URL → click count, timestamps)

Operations:

  • Create short URL (write)
  • Redirect (read)
  • Get statistics (read)

Components:

  • URL Shortener API: Handles creation requests
  • Redirect Service: Handles redirect requests
  • Database: Stores URL mappings
  • Cache: Fast redirects (Redis)
  • Analytics Service: Processes click events
  • Analytics Database: Stores statistics

Interfaces:

  • API → Database (write URL mapping)
  • Redirect Service → Cache (read URL mapping)
  • Redirect Service → Message Queue (publish click event)
  • Analytics Service → Message Queue (consume click event)
  • Analytics Service → Analytics Database (store statistics)

Thinking with Flows Instead of Code

What Are Flows?

Flows describe how data moves through the system:

  • Request flow: How a request travels through components
  • Data flow: How data is stored, retrieved, and processed
  • Event flow: How events are published and consumed

Example: E-commerce Order Flow

Request Flow:

User → Load Balancer → API Gateway → Order Service → 
Payment Service → Inventory Service → Notification Service

Data Flow:

Order Request → Order Service → Order Database
Payment Request → Payment Service → Payment Database
Inventory Update → Inventory Service → Inventory Database

Event Flow:

Order Created → Message Queue → 
  → Email Service (send confirmation)
  → Analytics Service (track metrics)
  → Inventory Service (update stock)

Why Flows Matter

Thinking in flows helps you:

  • Identify bottlenecks: Where does data get stuck?
  • Design for scale: Where do we need caching, queues, or parallel processing?
  • Design for failure: What happens if a component fails?
  • Optimize performance: Where can we reduce latency?

Identifying Bottlenecks Early

Common Bottlenecks

  1. Database: Single database can't handle high read/write load
  2. API: Single API server can't handle high request volume
  3. Network: High latency between services
  4. CPU: Expensive computations block other requests
  5. Memory: Large data structures consume too much memory

How to Identify Bottlenecks

  1. Analyze the flow: Where does data get processed?
  2. Estimate load: How many requests per second?
  3. Identify single points: What components can't scale?
  4. Calculate capacity: Can each component handle the load?

Example: Social Media Feed

Flow:

User Request → API → Database → Return Feed

Bottleneck Analysis:

  • API: Can handle 10K requests/second (OK)
  • Database: Can handle 1K queries/second (BOTTLENECK!)
  • Network: 10ms latency (OK)

Solution:

  • Add cache (Redis) to reduce database load
  • Pre-compute feeds for active users
  • Use read replicas for database

New Flow:

User Request → API → Cache (80% hit rate) → Return Feed
Cache Miss → Database → Cache → Return Feed

Designing for Scale from Day 1

Scale Thinking

Even for MVPs, think about:

  • 10x scale: What happens at 10x current load?
  • 100x scale: What happens at 100x current load?
  • Bottlenecks: Where will the system break?
  • Scaling strategy: How do we scale each component?

Example: URL Shortener MVP

Initial Scale: 1K URLs/day, 10K redirects/day

10x Scale: 10K URLs/day, 100K redirects/day

  • Database: Still OK (single instance)
  • API: Still OK (single instance)
  • Cache: Still OK (single Redis instance)

100x Scale: 100K URLs/day, 1M redirects/day

  • Database: BOTTLENECK (need read replicas)
  • API: Still OK (single instance)
  • Cache: Still OK (single Redis instance)

1000x Scale: 1M URLs/day, 10M redirects/day

  • Database: Need sharding
  • API: Need load balancing
  • Cache: Need Redis cluster

Design Strategy:

  • Start simple (single instance)
  • Design for horizontal scaling (stateless services)
  • Plan for sharding (use hash-based sharding)
  • Plan for caching (cache-aside pattern)

Choosing Between Synchronous vs Asynchronous Flows

Synchronous Flow

Characteristics:

  • Request → Process → Response (all in one flow)
  • Client waits for response
  • Simple to implement
  • Lower latency for simple operations

When to Use:

  • Simple operations (< 100ms)
  • Need immediate response
  • Low volume (< 1K requests/second)
  • Can't tolerate delays

Example: User login

User → API → Database → Return Auth Token

All happens synchronously, user gets immediate response.

Asynchronous Flow

Characteristics:

  • Request → Queue → Process (separate flows)
  • Client doesn't wait for processing
  • More complex to implement
  • Better for high volume

When to Use:

  • Long-running operations (> 1 second)
  • High volume (> 10K requests/second)
  • Can tolerate delays
  • Need to handle spikes

Example: Image processing

User → API → Queue → Return Job ID
Worker → Queue → Process Image → Store Result
User → API → Check Job Status → Return Result

User gets immediate response (job ID), processing happens asynchronously.

Decision Framework

Choose Synchronous when:

  • Operation is fast (< 100ms)
  • Need immediate response
  • Low volume
  • Simple use case

Choose Asynchronous when:

  • Operation is slow (> 1 second)
  • High volume
  • Can tolerate delays
  • Need to handle spikes

Real-World Example: Instagram Photo Upload

Synchronous (Not Used):

User → API → Process Image → Store → Return URL

Problem: User waits 5-10 seconds for image processing.

Asynchronous (Used):

User → API → Queue → Return Job ID
Worker → Queue → Process Image → Store → Update Status
User → API → Check Status → Return URL

Benefit: User gets immediate response, processing happens in background.


Thinking Aloud Like a Senior Engineer

Let me walk you through how I'd actually break down a vague problem into components. This is the messy, real-time reasoning that happens before you have a clean architecture.

Problem: "Build a chat system that supports 1-on-1 and group messaging."

My first instinct: "I'll create a ChatService class with sendMessage() and receiveMessage() methods. Simple, right?"

But wait—that's coding thinking, not architecture thinking. Let me step back and think about the system, not the code.

What are we actually building? A chat system. What does that mean?

  • Users send messages
  • Messages are delivered in real-time
  • Messages are stored for history
  • Users can see who's online

Let me break this into components:

  • API Layer: REST API for sending messages, getting history
  • Real-time Layer: WebSocket server for real-time delivery
  • Storage Layer: Database for message history
  • Presence Service: Track online/offline users

My next thought: "Do I need all of these? Can I simplify?"

Actually, yes I need them all:

  • API for sending messages (can't use WebSocket for everything)
  • WebSocket for real-time (HTTP polling is too slow)
  • Database for history (users need to see past messages)
  • Presence for online status (users want to know who's online)

Now, let me think about flows:

  • Send message flow: User → API → Database → WebSocket → Recipient
  • Get history flow: User → API → Database → Return messages
  • Presence flow: User connects → Update presence → Notify others

But wait—the send message flow has a problem. If I write to database first, then send via WebSocket, what if WebSocket fails? The message is stored but not delivered.

I'm thinking: "Should I send via WebSocket first, then store? Or store first, then send?"

Actually, I should do both in parallel: Store in database and send via WebSocket. If WebSocket fails, the message is still stored, and the recipient can fetch it later.

But that creates a new problem: What if database write succeeds but WebSocket fails? The message is stored but not delivered in real-time. That's okay—the recipient can fetch it.

Now, let me think about scale: "What happens at 1M users, 100M messages/day?"

Bottleneck analysis:

  • API: Can handle 10K requests/second (OK with load balancing)
  • Database: 100M messages/day = ~1.2K messages/second (OK with proper indexing)
  • WebSocket: 1M concurrent connections (BOTTLENECK!)

I need to handle WebSocket scale: "I can't have 1M connections on a single server. I need multiple WebSocket servers, and I need a way to route messages to the correct server."

My solution: "I'll use a message queue (RabbitMQ/Kafka) to route messages. When a message is sent, it goes to the queue, and the WebSocket server that has the recipient connected picks it up."

But how do I know which server has which user? "I need a presence service that tracks which WebSocket server has which user connected."

This is getting complex: "Let me simplify. I'll use Redis to track presence (user → server mapping), and use a message queue to route messages."

Final architecture:

  • API → Database (store message)
  • API → Message Queue (publish message event)
  • WebSocket Server → Message Queue (consume message event)
  • WebSocket Server → Redis (check presence, get server)
  • WebSocket Server → Recipient (send message)

This is the trade-off I'm making: More complex architecture (queue, Redis, multiple servers) for better scalability. I'm accepting complexity for scale.

Notice how I didn't jump to code. I thought about components, flows, bottlenecks, and scale. That's architecture thinking.


How a Senior Engineer Thinks

A senior engineer approaches architecture systematically:

  1. Break down the problem: What are we building? What are the components?
  2. Think in flows: How does data move? Where are the bottlenecks?
  3. Design for scale: What happens at 10x, 100x scale?
  4. Choose patterns: Synchronous vs asynchronous? Cache vs database?
  5. Design for failure: What happens if a component fails?
  6. Make trade-offs explicit: Why did we choose this architecture?

Example: Design a Notification System

Step 1: Break Down Problem

  • Components: API, Queue, Workers, Email Service, SMS Service, Push Service
  • Entities: Notifications, Users, Preferences

Step 2: Think in Flows

  • Request flow: User → API → Queue
  • Processing flow: Queue → Worker → Service → User
  • Data flow: API → Database (store notification), Worker → Database (update status)

Step 3: Design for Scale

  • 1M notifications/day = ~12 notifications/second (manageable)
  • Spikes: 1M notifications in 1 minute = ~16K/second (need queue)
  • Solution: Queue + multiple workers

Step 4: Choose Patterns

  • Asynchronous (high volume, can tolerate delays)
  • Queue (RabbitMQ/Kafka) for reliability
  • Multiple workers for parallel processing

Step 5: Design for Failure

  • Queue failure: Reject new requests, return error
  • Worker failure: Notifications remain in queue, another worker picks up
  • Service failure: Retry with exponential backoff

Step 6: Make Trade-offs Explicit

  • Chose async for scalability (accepting delay)
  • Chose queue for reliability (accepting complexity)
  • Chose multiple workers for throughput (accepting coordination overhead)

Real-World Example: Uber's Ride Matching System

Problem: Match riders with drivers in real-time.

Architecture Thinking:

  1. Break Down Problem:

    • Components: Matching Service, Location Service, Notification Service, Payment Service
    • Entities: Riders, Drivers, Rides, Locations
  2. Think in Flows:

    • Request flow: Rider → API → Matching Service → Driver
    • Location flow: Driver → Location Service → Update Location
    • Matching flow: Matching Service → Find Nearby Drivers → Notify Driver
  3. Design for Scale:

    • Millions of riders and drivers
    • Real-time location updates (high frequency)
    • Low latency matching (< 5 seconds)
    • Solution: Geospatial database (Redis Geo), Real-time matching service
  4. Choose Patterns:

    • Synchronous matching (need immediate response)
    • Geospatial indexing (fast location queries)
    • Real-time updates (WebSocket/polling)
  5. Design for Failure:

    • Matching service failure: Fallback to simpler algorithm
    • Location service failure: Use cached locations
    • Notification failure: Retry with exponential backoff
  6. Trade-offs:

    • Chose geospatial database for fast queries (accepting memory cost)
    • Chose real-time matching for user experience (accepting complexity)
    • Chose synchronous flow for low latency (accepting lower throughput)

Best Practices

  1. Always break down problems: Don't jump to code, break into components first
  2. Think in flows: How does data move? Where are bottlenecks?
  3. Design for scale: What happens at 10x, 100x scale?
  4. Choose patterns wisely: Synchronous vs asynchronous? Cache vs database?
  5. Design for failure: What happens if a component fails?
  6. Make trade-offs explicit: Why did we choose this architecture?

Common Interview Questions

Beginner

Q: What is architecture thinking?

A: Architecture thinking is the ability to break down vague problems into concrete components, think in flows instead of code, identify bottlenecks early, and design for scale from day 1. It's the skill that differentiates junior engineers from senior ones.


Intermediate

Q: How do you break down a vague problem into components?

A: I follow these steps:

  1. Identify core entities (users, data, operations)
  2. Identify operations (what must the system do?)
  3. Map to components (which components handle which operations?)
  4. Design interfaces (how do components communicate?)
  5. Think in flows (how does data move?)

Senior

Q: You're designing a system that needs to handle 1B requests/day. How do you approach the architecture?

A: I break it down systematically:

  1. Break down the problem: Identify components (API, storage, caching, queues)
  2. Think in flows: How does data move? Where are bottlenecks?
  3. Design for scale:
    • 1B requests/day = ~11.5K requests/second
    • Need load balancing, caching, horizontal scaling
  4. Choose patterns:
    • Asynchronous processing for high volume
    • Cache for hot data (reduce database load)
    • Queue for spikes (handle traffic bursts)
  5. Design for failure: Fallback mechanisms, retries, graceful degradation
  6. Make trade-offs explicit: Chose async for scale, cache for performance, queue for reliability

Summary

Architecture thinking is about decomposing problems into components and flows:

  • Break down vague problems: Convert requirements into components
  • Think in flows: How does data move? Where are bottlenecks?
  • Design for scale: What happens at 10x, 100x scale?
  • Choose patterns: Synchronous vs asynchronous? Cache vs database?
  • Design for failure: What happens if a component fails?
  • Make trade-offs explicit: Why did we choose this architecture?

Key takeaways:

  • Always break down problems into components
  • Think in flows, not code
  • Design for scale from day 1
  • Choose patterns wisely
  • Design for failure
  • Make trade-offs explicit

FAQs

Q: How do I learn architecture thinking?

A: Practice:

  • Break down real-world systems (Instagram, Uber, Netflix)
  • Identify components and flows
  • Understand why they made certain choices
  • Practice designing systems from scratch

Q: Do I need to know specific technologies?

A: Not necessarily. Architecture thinking is about concepts (components, flows, patterns), not specific technologies. However, knowing common technologies (databases, caches, queues) helps you make informed decisions.

Q: How do I know if my architecture is good?

A: A good architecture:

  • Breaks down the problem clearly
  • Identifies all components and flows
  • Designs for scale
  • Handles failures gracefully
  • Makes trade-offs explicit

Q: Can I use architecture thinking for small projects?

A: Yes. Even for small projects, breaking down problems into components and thinking in flows will help you build better systems. You don't need to over-engineer, but you should think systematically.

Q: How do I communicate architecture thinking in interviews?

A:

  • Start by breaking down the problem
  • Identify components and flows
  • Explain your design decisions
  • Make trade-offs explicit
  • Use diagrams to visualize your thinking

Q: What's the difference between architecture thinking and system design?

A: Architecture thinking is the mental framework (how to think), while system design is the actual process (what to build). Architecture thinking helps you approach system design systematically.

Q: How long does it take to master architecture thinking?

A: It's a continuous learning process. Start with the basics (breaking down problems, thinking in flows), then practice with real problems, study real-world systems, and iterate. Most engineers see significant improvement after 3-6 months of focused practice.

Keep exploring

Design thinking works best when combined with practice. Explore more topics or apply what you've learned in our system design practice platform.