← All posts
8 min read

Choosing the Simplest Useful Design: Complexity Must Solve a Real Problem

#technology#software-design#simplicity#architecture
📑 On this page

Software can fail through underengineering: missing reliability, security, or capacity that was clearly required.

It can also fail through overengineering: adding layers, services, abstractions, and platforms for problems that do not yet exist.

Choose the least complex design that meets known requirements and risks while preserving reasonable paths for likely change.

Simple does not mean careless. Useful simplicity includes the safeguards the problem genuinely requires.

A concrete example: a daily report

A team needs one report every morning from a relational database.

Possible design A:

  • Scheduled SQL query
  • Generate CSV
  • Store it securely
  • Notify recipients

Possible design B:

  • Stream every database change
  • Operate a distributed event platform
  • Maintain a real-time analytics store
  • Build several transformation services

If daily freshness is sufficient, design B pays ongoing cost for a latency requirement nobody has.

A scheduled query is not primitive. It is appropriate.

Start with explicit requirements

List:

  • Users
  • Required behavior
  • Data volume
  • Peak load
  • Latency
  • Availability
  • Security and privacy
  • Delivery time
  • Team and budget

Complexity should trace to one of these forces.

If a component cannot answer "Which requirement makes this necessary?", challenge it.

Requirements can change, but present evidence is a better foundation than imagined maximum futures.

Establish a baseline

Begin with the simplest plausible solution and measure it.

Examples:

  • One process
  • One relational database
  • One queue
  • One region
  • One maintained framework

Then test:

  • Does it meet latency?
  • Does it survive required failures?
  • Can the team deploy it safely?
  • Does it protect data?
  • What is the cost?

A baseline reveals the actual gap.

Without it, architecture comparisons become claims without evidence.

Complexity has carrying cost

Every added element creates:

  • Configuration
  • Documentation
  • Monitoring
  • Security updates
  • Failure modes
  • Training
  • On-call responsibility
  • Upgrade work

A microservice is not only its source code. A cache is not only a fast lookup. A queue is not only asynchronous delivery.

Estimate lifecycle cost, not setup excitement.

The team must be able to understand and operate the design during an incident.

Essential versus accidental complexity

Essential complexity belongs to the problem:

  • Tax rules differ by jurisdiction.
  • Money transfers require audit.
  • Offline edits can conflict.
  • Medical data needs strong privacy.

Accidental complexity comes from the chosen solution:

  • Unnecessary services
  • Duplicate data models
  • Custom frameworks
  • Excessive configuration
  • Poor interfaces

Architecture cannot remove essential complexity, but should represent it clearly without adding more than necessary.

Do not label a difficult business rule as technical mess simply because it cannot be made trivial.

Prefer reversible decisions

Some decisions are easy to change:

  • Internal library
  • Local file structure
  • Private function names

Others are expensive:

  • Public API
  • Stored data model
  • Customer identifiers
  • Cloud region
  • Encryption strategy
  • Service boundary

Delay irreversible decisions until enough evidence exists, while avoiding paralysis.

Use interfaces and migrations to preserve options around consequential choices.

Reversibility is practical flexibility, not abstract future-proofing.

Modular does not require distributed

A modular monolith can separate:

  • Orders
  • Customers
  • Payments
  • Notifications

through code and data-access boundaries while deploying one application.

This provides:

  • Clear ownership
  • Focused tests
  • Easier future extraction
  • Simple transactions and operations

Network boundaries should appear when independent deployment, scaling, security, or ownership justifies them.

Use modules to buy design clarity without paying distributed-system cost prematurely.

Use boring technology deliberately

A mature, familiar technology can provide:

  • Known failure modes
  • Reliable documentation
  • Available skills
  • Stable tooling
  • Easier hiring
  • Proven operations

"Boring" does not mean old or inferior. It means its costs are understood.

New technology is justified when it solves a real limitation or creates substantial value.

Adoption should include a learning plan, exit strategy, operational ownership, and comparison with the existing option.

Build for likely change

Future-proofing every possibility creates abstractions for guesses.

Instead, identify likely change:

  • Tax providers may change.
  • Product fields will grow.
  • Traffic may double.
  • One partner API will be replaced.

Create boundaries around those credible changes.

Avoid generic engines that support every hypothetical workflow when only two known workflows exist.

The best extension point often appears after the second concrete case, when similarities and differences are visible.

Do the simplest safe thing

Simplicity must include:

  • Authentication
  • Authorization
  • Input validation
  • Backups
  • Error handling
  • Observability
  • Accessibility
  • Required compliance

Leaving these out is not a simple design. It is an incomplete design that transfers cost to users and operators.

The least complex useful solution includes the quality attributes necessary for its impact.

A prototype and a production payment system have different minimums.

Use spikes for uncertainty

A time-boxed technical spike can answer:

  • Can the database meet the query target?
  • Does the vendor API support the workflow?
  • Can the model reach acceptable quality?
  • Is migration feasible?

The output is evidence, not necessarily production code.

Define:

  • Question
  • Time limit
  • Success threshold
  • What will be discarded

Spikes reduce uncertainty without forcing the full architecture to commit early.

Evolutionary architecture

An evolutionary design supports incremental change through:

  • Automated tests
  • Versioned migrations
  • Clear modules
  • Backward-compatible interfaces
  • Observability
  • Small deployments
  • Architecture fitness checks

It does not attempt to predict every future.

It keeps feedback fast enough that the design can respond when reality changes.

This is more reliable than either rigid upfront perfection or unstructured improvisation.

Scale when measurements demand it

Before adding distributed capacity, identify:

  • Bottleneck
  • Current utilization
  • Expected growth
  • Required headroom
  • Cost of scaling up
  • Cost of scaling out

A larger database instance may solve a two-year need more safely than immediate sharding.

Optimization can remove unnecessary work before adding machines.

Scale architecture should preserve correctness, not merely increase request count.

Avoid speculative abstraction

An abstraction is valuable when it:

  • Encapsulates volatility
  • Removes meaningful duplication
  • Clarifies a domain concept
  • Enables testing
  • Enforces a rule

It is harmful when it:

  • Hides important behavior
  • Has only one speculative implementation
  • Requires more concepts than the code it replaces
  • Creates generic names with no domain meaning

Wait until the shared concept is understood.

Duplication can be cheaper than the wrong abstraction while the problem is still being learned.

Make failure proportional

Complex resilience should follow impact.

A personal blog may accept:

  • Brief downtime
  • Manual restore
  • One region

A payment ledger may require:

  • Strong transactions
  • Audit
  • Redundancy
  • Reconciliation
  • Tested recovery

Do not copy the architecture of a global bank into a hobby project.

Do not use hobby-project safeguards for a hospital.

Simplicity is contextual.

Delete and consolidate

Complexity grows through accumulation.

Regularly ask:

  • Is this feature used?
  • Is this service independently valuable?
  • Can these flags be removed?
  • Does this data copy still have a purpose?
  • Can two tools become one?
  • Is this abstraction helping current change?

Deleting unused code and infrastructure reduces attack surface, cost, and cognitive load.

Architecture work includes subtraction.

Decision records and triggers

Record why the simple choice is sufficient and what evidence would trigger change.

Example:

Use one relational database.
Reconsider partitioning when sustained write utilization exceeds 60%,
storage growth threatens the three-year target, or regional latency
violates the agreed SLO.

This prevents recurring speculative debate and avoids treating today's design as permanent.

Triggers connect future complexity to observable need.

A practical design sequence

  1. Define user outcomes and quality requirements.
  2. Identify essential complexity and high-impact risks.
  3. Choose a simple baseline.
  4. Test uncertain assumptions.
  5. Add safeguards required by impact.
  6. Measure bottlenecks.
  7. Keep modules and data ownership clear.
  8. Record consequential decisions and change triggers.
  9. Evolve through small verified steps.
  10. Remove complexity that no longer earns its cost.

This sequence makes sophistication a response to evidence.

Knowledge check

  1. Why might a scheduled query be better than a streaming platform?
  2. How does essential complexity differ from accidental complexity?
  3. Why is a modular monolith useful for preserving options?
  4. What makes a simple design incomplete rather than appropriately simple?
  5. How do decision triggers help future architecture changes?

The one idea to remember

Choose the least complex design that satisfies real behavior, quality, and risk requirements. Preserve likely change through clear boundaries and evidence-based triggers, then add complexity only when it solves a measured problem.