paulserban.eu

Writing Edition

Paul Serban

Writing, snippets & book notes

What Is Software Architecture? A Practitioner's Take on the Skill That Separates Coders From Engineers

Untangling design from architecture, why the distinction matters, and how to actually get better at both

Introduction

Ask ten engineers to define "software architecture" and you'll get ten different answers, most of them half-right. Some will describe it as "the diagrams," others as "whatever the architect does," and a few will just shrug and say "it's design, but bigger." That last answer is closer to the truth than most people realize, but it's still incomplete. After spending years looking at this topic from different angles - as an implementer following someone else's architecture, as a lead making the decisions myself, and as a reader of postmortems and engineering blogs - I've landed on a definition that I think holds up: architecture is the discipline of shaping how a system is structured, how it behaves under stress, and how it evolves over time, and it sits squarely at the intersection of technical depth, strategic thinking, and communication.

This piece is my attempt to lay out that view clearly, distinguish it from the closely related (and often conflated) discipline of software design, and give engineers at any level a concrete sense of what to study, what to practice, and what to avoid. None of this is exotic or novel - it draws on well-established ideas from people like Martin Fowler, Robert C. Martin, Eric Evans, and Simon Brown - but the synthesis is mine, shaped by what actually turned out to matter when systems were under load, teams were growing, and requirements kept shifting underneath us.

Software Design vs. Software Architecture

The cleanest way I've found to separate these two concepts is by scope and altitude. Software design is tactical: it's the structuring of code and components to solve a specific, bounded problem. It's the classes you write, the interfaces you expose, the modules you carve out, and the patterns and algorithms you reach for to keep that code understandable and changeable. Design decisions are usually reversible at relatively low cost - you can refactor a class hierarchy or swap an algorithm without rewriting the system around it.

Software architecture, by contrast, is strategic. It's the high-level structuring of an entire system: which components exist, how they talk to each other, what technologies underpin them, and how the whole thing scales, tolerates failure, and evolves as the business changes. Architectural decisions are expensive to reverse - choosing a monolith versus microservices, a relational versus event-driven data model, or a synchronous versus asynchronous communication style all have consequences that ripple through the codebase for years.

I like the analogy of a building: software design is the blueprint for a single room - where the outlets go, how the closet is laid out, what materials line the walls. Software architecture is the blueprint for the entire building - how many floors it has, where the load-bearing walls sit, how utilities are routed, and whether it can support an extra floor being added five years from now. You can redesign a room without touching the rest of the building, but you can't add a floor without engaging with the architecture. Both disciplines matter, and in practice they blend into each other constantly, but keeping the distinction in mind helps you reason about which kind of decision you're actually making at any given moment.

Why This Skill Is Critical

It's tempting to file architecture under "nice to have" or "something senior people worry about," but that framing undersells how directly it affects outcomes. Poor architecture doesn't scale, and no amount of extra compute or clever caching fixes a fundamentally bad decision made two years earlier. You can throw more servers at a system with a chatty synchronous call chain, but eventually the coupling itself becomes the bottleneck - not CPU, not memory, but the shape of the system. Teams that ignore this end up rewriting systems from scratch, which is far more expensive than getting the shape roughly right the first time.

The other reasons compound on top of that scalability point. Good design and architecture choices make code easier to change, extend, and debug - which directly affects maintainability. A solid architectural foundation keeps teams moving fast instead of getting bogged down paying interest on technical debt, which is why velocity and architecture are so tightly linked even though they sound like unrelated concerns. Robust architecture also builds systems that fail gracefully and recover instead of cascading into full outages, which matters enormously once a system has real users depending on it. And perhaps most overlooked: architecture is what keeps software evolving in sync with business goals - a system architected around yesterday's requirements quietly becomes a drag on tomorrow's roadmap, even if every individual class inside it is well-written.

The Anatomy of Software Architecture

Architecture isn't one skill - it's a bundle of related concerns that show up together in almost every real system. Understanding the shape of that bundle helps you know what to study and where your own gaps are.

Modularity and separation of concerns is the foundation: avoiding monoliths and entangled code by drawing clean boundaries between parts of the system that change for different reasons. This is where concepts like bounded contexts (from Domain-Driven Design) and the single responsibility principle earn their keep - not as academic ideals, but as practical tools for keeping a codebase navigable as it grows. Closely related is the layer of design principles and patterns: SOLID principles, GRASP responsibility patterns, and Domain-Driven Design tactical and strategic patterns all give you a shared vocabulary for reasoning about coupling, cohesion, and responsibility that holds up across languages and paradigms.

Above that sits system decomposition - the question of whether a system should be a monolith, a set of microservices, a modular monolith, or something serverless. None of these is inherently superior; each trades operational complexity for deployment independence and team autonomy differently. Once you've decomposed a system, you need communication protocols to let the pieces talk: REST and GraphQL for synchronous request/response patterns, gRPC where performance and strong typing matter, and message queues (Kafka, RabbitMQ, SQS) where you want decoupling and asynchronous processing. Alongside that sits data modeling - schema design, CQRS, event sourcing, and caching strategy - which is arguably where the hardest and most consequential architectural decisions live, since data models are the most expensive part of a system to change once real data is flowing through them.

Finally, two areas act as constraints on everything else. Non-functional requirements - performance, reliability, security, and observability - aren't features you bolt on later; they shape the architecture from day one, because a design that ignores them tends to require a rewrite once they become urgent. And trade-offs and decision-making is the meta-skill that ties it all together: understanding the CAP theorem, the tension between latency and throughput, and the tension between consistency and availability is what lets you make an intentional choice instead of an accidental one. Good architects don't avoid trade-offs - they name them explicitly and document why one side was chosen, which is where tooling like Architecture Decision Records (ADRs) and the C4 model for diagramming systems (Context, Containers, Components, Code) become genuinely useful rather than bureaucratic overhead.

A Practical Example: Making a Trade-off Explicit

Abstract principles are easier to internalize with a concrete example, so consider a common decision: should a service validate incoming requests synchronously and reject bad ones immediately, or accept them and process validation asynchronously through a queue? This is a small decision on the surface, but it's a genuine architectural trade-off between latency, consistency, and fault tolerance, and it's worth walking through explicitly rather than defaulting to whichever pattern is trendy.

The synchronous approach gives immediate feedback to the caller and keeps the system easier to reason about, at the cost of tighter coupling and lower throughput under load. The asynchronous approach decouples the producer from the consumer and smooths out traffic spikes, at the cost of eventual consistency and more operational complexity (dead-letter queues, retries, idempotency). Below is a simplified TypeScript sketch of the asynchronous path, showing the kind of idempotency handling that this trade-off actually demands in practice - the part that's easy to skip in a diagram but expensive to skip in production:

interface OrderRequest {
  orderId: string;
  customerId: string;
  items: { sku: string; quantity: number }[];
}

class OrderIngestionService {
  constructor(
    private queue: MessageQueue,
    private idempotencyStore: IdempotencyStore
  ) {}

  async accept(request: OrderRequest): Promise<{ accepted: boolean }> {
    // Guard against duplicate submissions before we ever touch the queue.
    const alreadyProcessed = await this.idempotencyStore.has(request.orderId);
    if (alreadyProcessed) {
      return { accepted: true }; // Idempotent: same result, no duplicate work.
    }

    await this.idempotencyStore.markPending(request.orderId);
    await this.queue.publish("orders.validate", request);

    return { accepted: true };
  }
}

The interesting part isn't the queue call itself - it's the idempotency store. Any architecture that chooses asynchronous processing to gain fault tolerance has implicitly signed up for the problem of duplicate delivery, and pretending otherwise is how "resilient" systems end up double-charging customers. This is the kind of detail that separates an architecture diagram from a working system.

Common Pitfalls

Most architectural failures I've seen aren't caused by ignorance of patterns - they're caused by a handful of recurring habits of mind. The first is over-engineering: introducing abstraction layers, plugin systems, or design patterns before there's a concrete problem that requires them. Premature abstraction is expensive because it adds indirection that has to be understood by everyone who touches the code afterward, and it's usually built around a guess about future requirements that turns out to be wrong. The second is failing to document decisions - teams that don't write down why they chose a particular database, message broker, or service boundary inevitably end up re-litigating the same debate a year later, or worse, "fixing" a deliberate trade-off because nobody remembers it was deliberate.

The third pitfall is ignoring the domain: architecture that's technically elegant but doesn't reflect how the business actually operates tends to fight the organization it's supposed to serve, forcing awkward workarounds everywhere the model and reality diverge. The fourth is reinventing the wheel - building custom solutions for problems that mature frameworks and tools already solve well, usually justified by a vague sense that "our case is different" without a concrete reason backing it up. And the fifth, especially common right now, is a one-size-fits-all mindset: treating microservices, event sourcing, or any other pattern as a default best practice rather than a tool suited to specific conditions. Microservices solve organizational scaling problems as much as technical ones; applying them to a five-person team's product is usually just distributed monolith with extra latency.

Growing Your Architecture Skills

Architecture is not a credential you earn once - it's a skill built through repeated exposure to real constraints, and there are a handful of practices that reliably accelerate it. Building many systems, especially small pet projects with real constraints like offline support or real-time synchronization, forces you to make trade-offs under pressure rather than in the abstract, which is where the lessons actually stick. Refactoring legacy code teaches design in a way greenfield work never can, because untangling someone else's entangled decisions is the fastest way to internalize what "good separation of concerns" actually buys you.

Studying real architectures is equally valuable - engineering blogs from companies like Netflix, Uber, and Shopify regularly publish detailed case studies of how they've evolved their systems, and reading these with a critical eye (what problem were they solving, what did they trade away) teaches pattern recognition that no textbook can substitute for. Learning design patterns deeply, rather than memorizing them, matters just as much: the goal isn't to name-drop the Strategy pattern in a code review, it's to recognize the shape of a problem that the pattern was designed to solve, and to reach for it only when that shape actually appears.

Two more habits round this out. Pairing with other engineers on architectural decisions reinforces something easy to forget: architecture is as much about communication and organizational alignment as it is about technology, since a technically perfect design that the team doesn't understand or buy into will erode in practice. And making a habit of diagramming systems before building them - even a rough C4-style context diagram - forces you to externalize assumptions early, when they're cheap to challenge, rather than discovering a mismatched mental model three sprints into implementation.

Progress through this skill tends to follow rough milestones, though people move through them at different paces and rarely in a straight line. A beginner knows basic patterns like MVC or Singleton and follows an existing architecture without necessarily understanding why it's shaped that way. An intermediate engineer applies SOLID principles, writes modular code, and understands system-level trade-offs even if they're not yet driving them. An advanced engineer designs new systems, leads architectural decisions, and - critically - documents those designs clearly enough for others to follow. An expert goes further still: coaching others, aligning architecture with business strategy, actively managing technical debt as a portfolio rather than a backlog item, and driving architectural evaluation across a team or organization.

Best Practices

Given all of the above, a few practices consistently separate architecture that ages well from architecture that becomes a liability. Start decisions from constraints, not preferences: before choosing a pattern, be explicit about the actual scale, team size, latency budget, and failure tolerance you're designing for, since most architectural mistakes come from optimizing for problems the system doesn't actually have yet. Document every non-trivial decision using something lightweight like an Architecture Decision Record - a short markdown file capturing the context, the decision, and the trade-offs considered is enough to save future teams from re-deriving the same reasoning or, worse, reversing a deliberate choice by accident.

Equally important is treating non-functional requirements as first-class inputs to the design rather than afterthoughts bolted on near launch. Performance targets, security boundaries, and observability requirements should shape component boundaries from the start, because retrofitting them later usually means touching every part of the system you were trying to avoid touching. And finally, revisit architecture periodically rather than treating it as a one-time decision: systems that started as the right shape for a five-person startup often need deliberate, incremental evolution as the team and the business scale, and the healthiest architectures are the ones with a built-in process for reconsidering their own assumptions.

Analogies and Mental Models

Beyond the building-blueprint analogy already mentioned, a few other mental models have consistently helped me reason about architecture under pressure. Thinking of a system as a city rather than a single building is useful once it grows past a certain size: individual buildings (services) can be renovated or rebuilt independently, but roads, utilities, and zoning laws (communication protocols, shared data contracts, organizational conventions) have to evolve much more carefully because everything depends on them. This is a good gut-check for whether a proposed change is a "design" change (renovate one building) or an "architecture" change (reroute a road that half the city depends on).

Another useful frame is thinking of architecture as an option, in the financial sense - every architectural decision either preserves or forecloses future options, and the value of a decision often lies less in what it enables today and more in what it keeps open for tomorrow. A monolith with clean internal module boundaries preserves the option to split into services later; a monolith with entangled modules forecloses it. Framing decisions this way makes it easier to justify investing in boundaries and interfaces even when there's no immediate need to split anything - you're not paying for microservices, you're paying for the option to have them later if the business ever needs it.

Key Takeaways

If you take nothing else from this piece, these five practices will move the needle fastest:

  1. Separate the altitude of your decisions. Before making a change, ask whether you're solving a design problem (bounded, reversible) or an architecture problem (system-wide, expensive to reverse) - the right process differs for each.
  2. Write an ADR for every non-trivial decision. A short, dated record of context and trade-offs is cheap now and invaluable in a year.
  3. Study real systems, not just patterns. Read engineering blogs from companies operating at the scale or complexity you're curious about, and ask what problem they were actually solving.
  4. Diagram before you build. Even a rough C4 context diagram surfaces mismatched assumptions while they're still cheap to fix.
  5. Match the architecture to the constraints you actually have, not the ones you might have someday - over-engineering for imagined future scale is one of the most common and expensive architectural mistakes.

The 80/20 of Software Architecture

If you strip away the long list of patterns, protocols, and frameworks, a small set of ideas produces most of the practical benefit. Understanding trade-offs explicitly - consistency versus availability, latency versus throughput, coupling versus autonomy - accounts for the majority of good architectural judgment, because almost every other decision is a specific instance of one of these tensions. Writing decisions down, even briefly, prevents the single most common failure mode: teams re-litigating or accidentally reversing choices nobody remembers making. And matching the solution's complexity to the problem's actual scale - resisting both over-engineering and under-engineering - probably prevents more architectural disasters than any specific pattern or tool choice ever will. Everything else in this article is useful, but these three habits alone will take an engineer further than memorizing every pattern in the GoF catalog.

Conclusion

Software architecture is not a title, a role reserved for people with "architect" in their job description, or a set of diagrams produced once at the start of a project and forgotten. It's a mindset - a habit of asking why the code is shaped the way it is, not just what the code does, and of recognizing that every structural decision trades something away in exchange for something else. Software design and software architecture are related but distinct disciplines: design is the tactical shaping of individual components, architecture is the strategic shaping of the system those components live inside, and both matter enormously, just at different altitudes.

The transition from coder to engineer, and eventually to architect, happens the moment you start noticing that altitude - the moment a bug isn't just a bug but a symptom of a coupling decision made months earlier, or a slow endpoint isn't just slow but revealing a data model that was never built for the query patterns it now serves. You don't need a title to start practicing this. You need pet projects with real constraints, legacy code to untangle, case studies to read critically, and the discipline to diagram and document before you build. Do that consistently, and the milestones - beginner, intermediate, advanced, expert - take care of themselves.

References

Resources