Software Engineering Architecture Lab

I Have a Problem: Software Architecture Diagnosis

Tick the code, design, data, scaling and team problems that hurt, get a short diagnosis of each, and one learning plan of lessons, simulations and patterns.

Code quality

My code is becoming messy

The code no longer explains itself, and several responsibilities are tangled in the same places. This is usually debt that built up one shortcut at a time, not one bad decision. Clear names, small functions, separated concerns and a test safety net let you clean it in small steps instead of a risky rewrite.

  1. Names and Functions That Explain Themselves — Level 2 · Clean Code
  2. Separation of Concerns: Untangling the God Service — Level 2 · Clean Code
  3. Tests That Let You Change Code Without Fear — Level 2 · Clean Code
  4. Refactoring in Small, Safe Steps — Level 2 · Clean Code
  5. Technical Debt: The Loan You Didn’t Know You Took — Level 2 · Clean Code
  6. DRY Is About Knowledge, Not Code — Level 1 · Foundations
  7. Orthogonality: Change One Thing, Break One Thing — Level 1 · Foundations

My service class is huge

One class is answering to many different groups of people: pricing, payments, shipping and notifications all change it for their own reasons. Split it by reason to change, not by line count, and move behaviour next to the data it protects. Splitting too finely has a cost too: you trade one big file for many tiny classes that are hard to follow.

  1. Single Responsibility: One Reason to Change — Level 3 · OO Design
  2. Separation of Concerns: Untangling the God Service — Level 2 · Clean Code
  3. Refactoring in Small, Safe Steps — Level 2 · Clean Code
  4. Encapsulation, Tell Don’t Ask & the Law of Demeter — Level 3 · OO Design
  5. Service Layer: One Front Door for Every Use Case — Level 5 · Enterprise Architecture
  6. Domain Services vs Application Services — Level 6 · DDD
  7. Facade — Pattern
  8. Domain Model — Pattern

Adding features keeps breaking old code

Parts of the system depend on each other in hidden ways, and there is no safety net that catches a break early. Tests catch the damage; orthogonal design, clear contracts and substitutable types stop a change from spreading in the first place. Tests take time to write, so start with the paths that lose money when they break.

  1. Tests That Let You Change Code Without Fear — Level 2 · Clean Code
  2. Orthogonality: Change One Thing, Break One Thing — Level 1 · Foundations
  3. Contracts, Assertions & Crashing Early — Level 1 · Foundations
  4. Open/Closed: Add Features Without Editing Old Code — Level 3 · OO Design
  5. Liskov Substitution: Subtypes That Keep Their Promises — Level 3 · OO Design
  6. Test-Driven Development in Practice — Level 2 · Clean Code
  7. Refactoring in Small, Safe Steps — Level 2 · Clean Code
  8. Design Pattern Playground — Simulation 18

Duplicate code everywhere

First ask whether it is duplicated *knowledge* (one business rule written in several places) or code that only looks similar but changes for different reasons. Merge the first into one place. Leave the second alone: forcing it together creates coupling that hurts more than the duplication did.

  1. DRY Is About Knowledge, Not Code — Level 1 · Foundations
  2. Orthogonality: Change One Thing, Break One Thing — Level 1 · Foundations
  3. Refactoring in Small, Safe Steps — Level 2 · Clean Code
  4. Entities vs Value Objects — Level 6 · DDD
  5. Template Method: One Checkout Skeleton, Many Variations — Level 4 · Design Patterns
  6. Composition Over Inheritance — Level 3 · OO Design
  7. Money — Pattern
  8. Template Method — Pattern

Design & modelling

Too many if/else statements

A variation (payment type, order status, customer tier) is encoded as conditionals spread across the code. Polymorphism puts each variant in one place: Strategy for interchangeable ways of doing a job, State for behaviour that follows a lifecycle, Chain of Responsibility for a line of checks. If there are two cases and they rarely change, a plain if/else is still the simplest answer.

  1. Open/Closed: Add Features Without Editing Old Code — Level 3 · OO Design
  2. Strategy: Pluggable Payment Methods — Level 4 · Design Patterns
  3. State: An Order Lifecycle Without the Giant Switch — Level 4 · Design Patterns
  4. Chain of Responsibility: Validation & Fraud Pipelines — Level 4 · Design Patterns
  5. Factory Method & Abstract Factory: Creating the Right Thing — Level 4 · Design Patterns
  6. Design Pattern Playground — Simulation 18
  7. Strategy — Pattern
  8. State — Pattern
  9. Special Case — Pattern

Adding a new payment type requires changing 5 classes

Knowledge about each payment method is spread across the codebase instead of living in one place. Put each method behind one interface (Strategy), make the core depend on that interface rather than on concrete gateways (Dependency Inversion), and let new methods plug in without editing existing classes (Open/Closed, Plugin). The price is more classes and wiring that is resolved at runtime, so add a startup check that every method has a handler.

  1. Strategy: Pluggable Payment Methods — Level 4 · Design Patterns
  2. Open/Closed: Add Features Without Editing Old Code — Level 3 · OO Design
  3. Interface Segregation & Dependency Inversion — Level 3 · OO Design
  4. Factory Method & Abstract Factory: Creating the Right Thing — Level 4 · Design Patterns
  5. Adapter: Taming Third-Party Payment Gateways — Level 4 · Design Patterns
  6. Architecture Styles and Their Trade-offs — Level 7 · Software Architecture
  7. Design Pattern Playground — Simulation 18
  8. Strategy — Pattern
  9. Plugin — Pattern
  10. Adapter — Pattern

Business rules are scattered across controllers and SQL

There is no agreed home for business logic, so it leaks into every layer. Give the application clear layers and one front door per use case, then choose deliberately how much logic belongs in a domain model versus simple transaction scripts. A rich model pays off only when the rules are genuinely complex.

  1. Layered Architecture: Presentation, Domain, Data — Level 5 · Enterprise Architecture
  2. Transaction Script vs Domain Model vs Table Module — Level 5 · Enterprise Architecture
  3. Service Layer: One Front Door for Every Use Case — Level 5 · Enterprise Architecture
  4. Entities vs Value Objects — Level 6 · DDD
  5. Aggregates: Consistency Boundaries That Protect Invariants — Level 6 · DDD
  6. Repository: A Collection-Like Home for Aggregates — Level 5 · Enterprise Architecture
  7. Service Layer — Pattern
  8. Domain Model — Pattern
  9. Transaction Script — Pattern

Everyone means something different by ‘Customer’

One model is trying to serve several parts of the business that use the same words for different things. Draw boundaries where the language changes (bounded contexts), give each its own model, and translate at the edges. The cost is some duplicated data and explicit mapping between contexts.

  1. Ubiquitous Language: Code That Speaks the Business — Level 6 · DDD
  2. Bounded Contexts: Where Words Keep One Meaning — Level 6 · DDD
  3. Context Maps: How Bounded Contexts Relate — Level 6 · DDD
  4. Strategic Design: Core, Supporting & Generic Subdomains — Level 6 · DDD
  5. Entities vs Value Objects — Level 6 · DDD
  6. DDD Bounded Contexts Workshop — Simulation 17
  7. Anti-Corruption Layer — Pattern
  8. SaaS Subscription Billing — Case study

Data & databases

My database is too slow

Measure before changing anything: usually a handful of queries are missing an index or loading far too much. Fix those first. Caching, read replicas and partitioning come later, and each one buys speed by adding a new kind of staleness or operational work.

  1. Indexes: Fast Reads, Slower Writes — Level 8 · Data Systems
  2. Unit of Work, Identity Map & Lazy Load — Level 5 · Enterprise Architecture
  3. Caching: Speed at the Cost of Truth — Level 8 · Data Systems
  4. Replication: Leaders, Followers & Failover — Level 8 · Data Systems
  5. Storage Engines: B-Trees vs LSM-Trees — Level 8 · Data Systems
  6. Partitioning: Hash, Range, Hotspots & Rebalancing — Level 8 · Data Systems
  7. Database Index — Simulation 2
  8. Cache: Hits, Misses, TTL & Invalidation — Simulation 3
  9. Database Internals Lab — Lab

My database is full of lock waits and contention

Many transactions want the same rows at the same time and hold their locks too long. Keep transactions short, never make network calls inside them, and choose optimistic or pessimistic locking on purpose. For truly hot rows, change the design: reserve stock in batches or split the counter.

  1. Transactions & What ACID Actually Promises — Level 8 · Data Systems
  2. Isolation Levels: Dirty Reads to Serializable — Level 8 · Data Systems
  3. Lost Updates: Optimistic vs Pessimistic Locking — Level 8 · Data Systems
  4. Optimistic vs Pessimistic Offline Locks — Level 5 · Enterprise Architecture
  5. Partitioning: Hash, Range, Hotspots & Rebalancing — Level 8 · Data Systems
  6. Transaction Isolation Anomalies — Simulation 9
  7. Lost Updates & Optimistic Locking — Simulation 10
  8. Flash Sale Inventory — Interview
  9. Transactions Lab — Lab

Two people edit the same thing and one change disappears

Two read-modify-write cycles overlapped, and the second write overwrote the first without knowing it existed. A version column (optimistic locking) detects the conflict at save time; a lock (pessimistic) prevents it but makes others wait. Pick based on how often conflicts really happen.

  1. Lost Updates: Optimistic vs Pessimistic Locking — Level 8 · Data Systems
  2. Optimistic vs Pessimistic Offline Locks — Level 5 · Enterprise Architecture
  3. Isolation Levels: Dirty Reads to Serializable — Level 8 · Data Systems
  4. Transactions & What ACID Actually Promises — Level 8 · Data Systems
  5. Lost Updates & Optimistic Locking — Simulation 10
  6. Optimistic Offline Lock — Pattern
  7. Pessimistic Offline Lock — Pattern

Users see stale data after updating

The read was served by a copy that had not caught up yet: a read replica behind on replication, or a cache entry that was never invalidated. Decide which reads must see the latest write (usually your own recent changes) and route those to the leader or bypass the cache. Everything else can tolerate a little staleness in exchange for speed.

  1. Replication Lag: When Your Own Write Disappears — Level 8 · Data Systems
  2. Caching: Speed at the Cost of Truth — Level 8 · Data Systems
  3. Replication: Leaders, Followers & Failover — Level 8 · Data Systems
  4. Consistency Models, CAP & Consensus — Level 8 · Data Systems
  5. Replication Lag: Read-Your-Writes — Simulation 8
  6. Cache: Hits, Misses, TTL & Invalidation — Simulation 3
  7. Cache-Aside — Pattern
  8. Caching Lab — Lab

Scale & performance

We cannot handle the traffic

Find the real bottleneck first: application CPU, the database, a hot key, or a slow dependency. Stateless instances behind a load balancer scale the app tier cheaply; the data tier is usually the harder limit and needs caching, replicas or partitioning. Shedding or limiting load on purpose is better than falling over.

  1. Load Balancing & Horizontal Scaling — Level 11 · Production
  2. Caching: Speed at the Cost of Truth — Level 8 · Data Systems
  3. Replication: Leaders, Followers & Failover — Level 8 · Data Systems
  4. Partitioning: Hash, Range, Hotspots & Rebalancing — Level 8 · Data Systems
  5. Reliability: SLOs, Error Budgets & Graceful Degradation — Level 11 · Production
  6. Load Balancing Algorithms — Simulation 15
  7. Hash vs Range Partitioning & Hotspots — Simulation 21
  8. Consistent Hashing Ring — Simulation 4
  9. Rate Limiter — Pattern
  10. Flash Sale Inventory — Interview

I can’t tell why checkout is slow

You cannot follow one request across the system, so you are guessing. Add correlation ids, distributed tracing and percentile metrics tied to an SLO, then debug with a method: reproduce, isolate, form a hypothesis, verify. Telemetry costs storage and attention, so collect what answers real questions.

  1. Observability: Logs, Metrics, Traces & SLOs — Level 9 · Microservices
  2. Debugging Like an Engineer — Level 1 · Foundations
  3. Reliability: SLOs, Error Budgets & Graceful Degradation — Level 11 · Production
  4. Unit of Work, Identity Map & Lazy Load — Level 5 · Enterprise Architecture
  5. Synchronous vs Asynchronous Communication — Level 9 · Microservices
  6. Timeouts, Retries & Retry Storms — Level 9 · Microservices
  7. Correlation Identifier — Pattern
  8. ShopFlow Checkout — Interview

Distributed systems

My service keeps failing

Remote calls can be slow, fail, or succeed without you finding out. Without timeouts, every slow dependency ties up your threads until nothing is left. Timeouts, careful retries, circuit breakers and bulkheads turn a dependency problem into a contained, visible degradation.

  1. The Trouble With Distributed Systems — Level 8 · Data Systems
  2. Timeouts, Retries & Retry Storms — Level 9 · Microservices
  3. Circuit Breakers & Bulkheads — Level 9 · Microservices
  4. Observability: Logs, Metrics, Traces & SLOs — Level 9 · Microservices
  5. Reliability: SLOs, Error Budgets & Graceful Degradation — Level 11 · Production
  6. Circuit Breaker States — Simulation 13
  7. Microservice Failure Cascade — Simulation 16
  8. Timeout — Pattern
  9. Circuit Breaker — Pattern
  10. Bulkhead — Pattern

One slow service takes everything down

In a synchronous call chain, the slowest link sets the speed for everyone. Bound every wait with a timeout, stop calling a sick dependency with a circuit breaker, give each dependency its own resources (bulkheads), and move non-critical work off the request path. Each of these adds configuration you must tune and test.

  1. Timeouts, Retries & Retry Storms — Level 9 · Microservices
  2. Circuit Breakers & Bulkheads — Level 9 · Microservices
  3. Synchronous vs Asynchronous Communication — Level 9 · Microservices
  4. Reliability: SLOs, Error Budgets & Graceful Degradation — Level 11 · Production
  5. The Trouble With Distributed Systems — Level 8 · Data Systems
  6. Retry Storm & Backoff with Jitter — Simulation 14
  7. Microservice Failure Cascade — Simulation 16
  8. Bulkhead — Pattern
  9. Graceful Degradation & Fallback — Pattern
  10. Retry with Backoff & Jitter — Pattern
  11. Break the System — Page

Customers are charged twice on retries

A timeout does not tell you whether the operation happened. Any retry, whether by the user, the client or a message consumer, can repeat it. Make the operation idempotent: the client sends an idempotency key, the server stores it under a unique constraint, and a repeat returns the first result instead of charging again.

  1. Idempotency: Making Retries Safe — Level 9 · Microservices
  2. Timeouts, Retries & Retry Storms — Level 9 · Microservices
  3. Consumer Groups & Delivery Semantics — Level 8 · Data Systems
  4. Enterprise Integration Patterns: Messaging Between Systems — Level 5 · Enterprise Architecture
  5. Idempotency Keys & Duplicate Payments — Simulation 22
  6. Idempotency Key — Pattern
  7. Idempotent Receiver — Pattern
  8. Payments Platform — Case study
  9. Payment Platform at 50,000 TPS — Interview

Kafka messages arrive out of order

Kafka keeps order only within a partition, not across a topic. If events for one order use different keys (or no key), they can land on different partitions and be consumed in any order. Key events by the entity id, remember that adding partitions changes where keys go, and make consumers tolerate the occasional duplicate or late event.

  1. Logs & Kafka: Partitions, Offsets and Ordering — Level 8 · Data Systems
  2. Consumer Groups & Delivery Semantics — Level 8 · Data Systems
  3. Idempotency: Making Retries Safe — Level 9 · Microservices
  4. Event Sourcing & CQRS — Level 8 · Data Systems
  5. Kafka Partitions & Keys — Simulation 5
  6. Consumer Groups & Rebalancing — Simulation 6
  7. Idempotent Receiver — Pattern
  8. Kafka Lab — Lab

I need a transaction across services

A local database transaction cannot cover another service’s database. Either keep data that must change atomically inside one service, or use a saga: a sequence of local transactions with compensating steps, with events published reliably through an outbox. Two-phase commit exists, but it ties every participant’s availability together.

  1. Transactions & What ACID Actually Promises — Level 8 · Data Systems
  2. Aggregates: Consistency Boundaries That Protect Invariants — Level 6 · DDD
  3. Distributed Transactions: 2PC, Sagas & the Outbox — Level 9 · Microservices
  4. Finding Service Boundaries & Splitting the Monolith — Level 9 · Microservices
  5. Domain Events: Things That Happened — Level 6 · DDD
  6. Saga: Compensating a Failed Checkout — Simulation 12
  7. Distributed Transaction: Two-Phase Commit — Simulation 11
  8. Saga — Pattern
  9. Transactional Outbox — Pattern
  10. Global Marketplace — Case study

Two services need the same data

Give each piece of data exactly one owner. Other services either ask the owner’s API (always fresh, but coupled at runtime) or keep a local copy updated by events (independent, but eventually consistent). A shared database is the quickest option and the hardest one to undo.

  1. Database per Service & Data Ownership — Level 9 · Microservices
  2. Bounded Contexts: Where Words Keep One Meaning — Level 6 · DDD
  3. Synchronous vs Asynchronous Communication — Level 9 · Microservices
  4. Domain Events: Things That Happened — Level 6 · DDD
  5. Event Sourcing & CQRS — Level 8 · Data Systems
  6. Batch vs Stream Processing & Data Pipelines — Level 8 · Data Systems
  7. Database per Service — Pattern
  8. Change Data Capture — Pattern
  9. CQRS — Pattern

Architecture

My monolith is getting huge

Size is rarely the real problem; missing boundaries are. A modular monolith with enforced module boundaries often removes most of the pain at a fraction of the cost of microservices. Extract a service later, where independent deployment or scaling is clearly worth the operational price.

  1. The Modular Monolith — Level 7 · Software Architecture
  2. Bounded Contexts: Where Words Keep One Meaning — Level 6 · DDD
  3. Coupling, Connascence & Architecture Quanta — Level 7 · Software Architecture
  4. Automated Architecture Tests with ArchUnit — Level 10 · Evolutionary Architecture
  5. Why Microservices Exist — and When Not to Use Them — Level 9 · Microservices
  6. Finding Service Boundaries & Splitting the Monolith — Level 9 · Microservices
  7. Architecture Evolution Stepper — Simulation 20
  8. Architecture Fitness Functions — Simulation 19
  9. Strangler Fig — Pattern

Service dependencies are exploding

Service boundaries probably follow technical layers or database tables instead of business capabilities, so every use case crosses many services. Redraw boundaries around bounded contexts, prefer events for notifications, and measure coupling so it stays visible. Merging services back together is a valid move too.

  1. Finding Service Boundaries & Splitting the Monolith — Level 9 · Microservices
  2. Bounded Contexts: Where Words Keep One Meaning — Level 6 · DDD
  3. Coupling, Connascence & Architecture Quanta — Level 7 · Software Architecture
  4. Context Maps: How Bounded Contexts Relate — Level 6 · DDD
  5. Synchronous vs Asynchronous Communication — Level 9 · Microservices
  6. Teams, Ownership & Conway’s Law — Level 9 · Microservices
  7. DDD Bounded Contexts Workshop — Simulation 17
  8. API Gateway — Pattern
  9. Backends for Frontends — Pattern

Our architecture is becoming rigid

The structure was decided once and then neither protected nor revisited. Make the important characteristics measurable with fitness functions, change the system in small reversible steps, and record decisions so they can be reopened when the context changes. Automated checks cost effort to write and maintain, so protect only what really matters.

  1. Architecture Erosion: How Good Designs Decay — Level 10 · Evolutionary Architecture
  2. Fitness Functions: Tests for Your Architecture — Level 10 · Evolutionary Architecture
  3. Automated Architecture Tests with ArchUnit — Level 10 · Evolutionary Architecture
  4. Incremental Change: Strangler Fig, Expand/Contract & Pipelines — Level 10 · Evolutionary Architecture
  5. Reversibility: Don’t Bet the Company on One Decision — Level 1 · Foundations
  6. Change Dimensions & the Last Responsible Moment — Level 10 · Evolutionary Architecture
  7. Architecture Decision Records — Level 7 · Software Architecture
  8. Architecture Fitness Functions — Simulation 19
  9. Strangler Fig — Pattern
  10. Watch the Architecture Evolve — Page

Delivery & team

Deploys are scary and break things

Big, infrequent, manual releases make every deploy risky. Automate the pipeline, ship smaller changes more often, separate deploying code from releasing features with flags, and make schema changes backwards compatible. Feature flags and canaries add their own complexity, so clean them up once a rollout is finished.

  1. Automate Everything: Version Control, Builds & Pipelines — Level 1 · Foundations
  2. Deployment: Containers, Kubernetes, Canary & Feature Flags — Level 9 · Microservices
  3. Incremental Change: Strangler Fig, Expand/Contract & Pipelines — Level 10 · Evolutionary Architecture
  4. Evolving Data: Schema Changes Without Downtime — Level 10 · Evolutionary Architecture
  5. Tests That Let You Change Code Without Fear — Level 2 · Clean Code
  6. Testing Microservices & Consumer-Driven Contracts — Level 9 · Microservices
  7. Reliability: SLOs, Error Budgets & Graceful Degradation — Level 11 · Production

Our tests are slow and flaky

Too much of the suite is slow, broad tests that share state. Push most checks down to fast unit tests, use real infrastructure (for example Testcontainers) only where it matters, replace cross-service end-to-end tests with contract tests, and fix or quarantine flaky tests instead of retrying them. Fewer broad tests means some integration risk moves to monitoring and canaries.

  1. Tests That Let You Change Code Without Fear — Level 2 · Clean Code
  2. Test-Driven Development in Practice — Level 2 · Clean Code
  3. Testing Microservices & Consumer-Driven Contracts — Level 9 · Microservices
  4. Interface Segregation & Dependency Inversion — Level 3 · OO Design
  5. Automate Everything: Version Control, Builds & Pipelines — Level 1 · Foundations
  6. Debugging Like an Engineer — Level 1 · Foundations

Nobody remembers why we built it this way

Decisions were made but their context and trade-offs were never written down. Short architecture decision records capture what was decided, why, and what it cost, so the next person can judge whether the reasons still hold. They only help if they are kept next to the code and written at the time of the decision.

  1. Architecture Decision Records — Level 7 · Software Architecture
  2. Trade-off Analysis: Thinking Like an Architect — Level 7 · Software Architecture
  3. What Architecture Really Is (and Isn’t) — Level 7 · Software Architecture
  4. Architecture Characteristics: The -ilities Under Pressure — Level 7 · Software Architecture
  5. ADR Workshop — Page
  6. Architecture Decision Simulator — Page