Architecture

Posted on Dec 23, 2025
  1. No problem, no requirements.
  2. No requirements, no use cases.
  3. No use cases, no logical boundaries.
  4. No logical boundaries, no architecture.
  5. No architecture, no efficient solution.
  6. No efficient solution, the system falls apart.

What is architecture?

Software architecture is how you organize a system so it can change safely and meet its goals. That’s it.

Most writing on architecture falls into two extremes. Either it’s abstract frameworks with diagrams that obscure more than they clarify. Or it’s “just build it bro” with no structure at all, leaving you to figure things out as you go. Both fail when the system needs to evolve.

Architecture must reduce the cost of future change. It must keep coupling under control so modifying one part doesn’t break everything else. It must make failure predictable and deployments safe.

Where to start: Requirements and use cases

Start here. Not with diagrams. Not with layers. Start with what the system is supposed to do.

Requirements define the problem. Use cases show how actors interact with the system to accomplish goals. An actor can be a user, an external system, a scheduled job - anything that triggers behavior.

If you skip this, you’re building based on guesses. Requirements and use cases ground everything in reality.

Functionalities: What does the system need to do?

Once you know the requirements and use cases, list the functionalities. These are the things the system must actually do.

For a web service that processes orders, functionalities might be:

  • Accept HTTP requests
  • Validate input
  • Check inventory
  • Calculate pricing
  • Process payment
  • Update database
  • Send confirmation email
  • Return response

Don’t organize these yet. Just list them. Get them out of your head and into writing. This is the “what.”

Logical boundaries: Who does what?

Now group functionalities into logical boundaries. A boundary is a responsibility assignment. Which functionalities belong together? Which should be separate?

The key question: who does what?

For the order processing service:

  • HTTP Handler: accept requests, return responses
  • Core Logic: validate input, check inventory, calculate pricing
  • Database Client: update database
  • Payment Service: process payment
  • Email Service: send confirmation email

Each boundary has a job. Boundaries should be cohesive - related things stay together. They should minimize coupling - a boundary shouldn’t need to know about internals of other boundaries.

This is where diagrams help. Draw boxes and arrows. Show who talks to whom. Not for documentation, but to see if your boundaries make sense. If the diagram is a mess, your thinking is a mess.

Development: Organizing the code

Now translate boundaries into code. Modules, components, subsystems - these are different granularity levels of the same concept. A module is a small unit of code. A subsystem is a collection of modules. An API is the interface between them.

What matters is that each logical boundary has a home in the codebase.

Each boundary should have:

  • A clear interface
  • Internal implementation hidden
  • Minimal surface area
  • Stable contract

Deep modules are better than shallow ones. A deep module does a lot behind a small interface. open(), read(), write(), close() is the classic example. Shallow modules expose internal complexity and force you to understand it.

The UNIX generalization principle exemplifies deep abstraction. “Everything is a file” reduces diverse operations—disk, network, terminals, pipes—to a unified interface. This hides substantial complexity behind a minimal surface, enabling composition and reducing cognitive load. A shallow alternative would expose separate APIs for each resource type, increasing complexity and reducing composability.

Dependencies should flow one way. If A depends on B, B shouldn’t depend on A. Cycles are coupling traps.

How you organize the code depends on use cases and requirements.

Deployment: How does it run

Architecture isn’t just code structure. It’s also how the system runs in reality.

Single process. Multi-threaded. Multi-process. Distributed. Each adds complexity. Each adds capability.

Start simple. A single process with a thread pool is enough for most small to medium services. Add processes or services only when you have a clear reason: isolation, independent scaling, different deployment cycles.

Distributed systems are an order of magnitude harder. Don’t go there until you have to.

Diagrams are tools, not architecture

MBSE (Model-Based Systems Engineering) has a lot of formalism. Activity diagrams. Internal block diagrams. Sequence diagrams. State machines. Some of this is useful. Most is overkill.

What’s useful:

  • Understanding the system
  • Seeing communication paths
  • Identifying actors and use cases
  • Exploring logical boundaries
  • Checking if your design makes sense

What’s not useful:

  • Treating diagrams as the deliverable
  • Formalism for its own sake
  • Every possible diagram type
  • Analysis paralysis

Draw diagrams to think. Then write code.

What Lisp taught me

When I discovered Lisp it changed how I think about systems for the better.

Lisp has code-as-data. Your code is a data structure you can manipulate. Macros let you extend the language itself. You’re not stuck with what the language gives you - you can adapt it to your problem.

This made me realize: rigid architectures exist because languages are rigid. If you can shape the language to your domain, boundaries become more fluid. You can build DSLs that express your problem directly.

I’m not saying “rewrite everything in Lisp.” I’m saying: the right abstraction level makes architecture simpler. Sometimes that’s a module. Sometimes that’s a language feature. Sometimes that’s a DSL.

Risk-driven design

How much design upfront? Risk-driven architecture says: design enough to address the main risks, then start building.

Big Design Up Front is waste. No design at all is technical debt. The middle ground is “just enough.”

Just enough means:

  • Requirements are clear
  • Use cases are defined
  • Functionalities are listed
  • Logical boundaries make sense
  • Major risks are addressed

Then build. Adjust as you learn.

Proof of Concepts: Build to understand

Don’t design everything in your head. Build small, throwaway versions to validate assumptions.

PoCs answer questions: Is this approach feasible? Does this library work as advertised? Is this performance achievable? Do these boundaries actually make sense?

Get something working first. A single process. A simple version. Understand the problem by solving it poorly, then refine.

Design decisions that look good on paper often break in reality. PoCs reveal the truth before you commit.

Work in sprints. Focus on one PoC per sprint. Validate assumptions, get feedback, iterate. Don’t spend months designing. Build, measure, learn. Adjust architecture based on what you discover.

Trade-offs: Multiple solutions exist

For any architectural problem, there are multiple valid solutions. Single process vs multi-process. SQL vs NoSQL. Monolith vs microservices. Sync vs async.

The question isn’t “which is best?” The question is “what are the trade-offs?”

Build a trade-off matrix. List options. Identify criteria: complexity, performance, operational cost, team capability, time to market. Score each option. Make the trade-offs explicit.

No solution is free. Every choice has costs. Architecture is about choosing which costs you’re willing to pay.

REST! Take breaks

When your brain is tired, it’s impossible to discern good solutions from bad ones.

Architecture requires clear thinking. If you’re stuck, step away. Sleep on it. Go for a walk. Come back fresh.

The best architecture insights often come when you’re not staring at the problem.

Books that helped

Design It! by Michael Keeling taught me collaborative design and risk-driven architecture. Architecture isn’t a solo activity. It presents as a survey a lot of material I’ve learned through the years in a concise manner.

Release It! by Michael Nygard taught me that production reality is harsh. Circuit breakers, bulkheads, timeouts, these are not an after thought. Your software will be under attack from the moment you deploy.

A Philosophy of Software Design by John Ousterhout taught me about deep modules and complexity hiding.

The Art of UNIX Programming taught me a lot about the great ideas from the creators of Unix.

Designing Data-Intensive Applications by Martin Kleppmann taught me about reliability and trade-offs in data systems.

The paradox I lived in

I spent a lot of time confused. MBSE says to model everything with diagrams. Pragmatic programmers say to just build. Lisp showed me that flexibility comes from the right tools, not more diagrams.

Here’s what I’ve landed on:

  • Requirements and use cases are non-negotiable
  • Functionalities come first, then boundaries
  • Diagrams help you think, they’re not the architecture
  • How you organize code depends on the problem
  • Simple systems deserve simple architectures
  • Add complexity only when needed

Architecture is not a framework. It’s not a diagram. It’s how you organize decisions so the system can evolve without falling apart.

Start with requirements. Define functionalities. Draw boundaries. Write code. Deploy. Iterate.

Keep it simple. Ship it.