Skip to content
ioob.dev
Go back

Software Architecture Part 1 — Why Architecture Matters

· 8 min read
Software Architecture Series (1/6)
  1. Software Architecture Part 1 — Why Architecture Matters
  2. Software Architecture Part 2 — Layered Architecture
  3. Software Architecture Part 3 — Hexagonal Architecture
  4. Software Architecture Part 4 — Clean Architecture and Onion: Commonalities and Differences Among Concentric Structures
  5. Software Architecture Part 5 — CQRS and Event-Driven: From Read/Write Separation to Event-Based Systems
  6. Software Architecture Part 6 — Modular Monolith: What You Can Try Before Microservices
Table of contents

Table of contents

Everything Works at First

When a project begins, everything moves fast. You can stuff controllers, service logic, and DB access code into a single file and it works. If there are only three features, that might even feel efficient.

The problems start from here. New requirements are added, the team grows, and deployment cadence accelerates. One day you modify the payment logic and the point accrual breaks. You try to add a notification feature, but it takes two days just to understand the order code. You want to write tests, but everything is tangled together and nothing can be verified without a DB.

This isn’t unique to any one team. It’s the universal fate of unstructured code.

The Dependency Graph of Unstructured Code

When code has no structure, what happens becomes clear in a diagram. Below is the typical dependency graph of code written without architecture.

flowchart TB
    OrderController["OrderController"] --> OrderService["OrderService"]
    OrderController --> PaymentService["PaymentService"]
    OrderController --> UserRepository["UserRepository"]
    OrderService --> PaymentService
    OrderService --> UserRepository
    OrderService --> NotificationService["NotificationService"]
    PaymentService --> OrderService
    PaymentService --> UserRepository
    PaymentService --> NotificationService
    NotificationService --> UserRepository
    NotificationService --> OrderService

    style OrderController fill:#ff6b6b,color:#fff
    style OrderService fill:#ff6b6b,color:#fff
    style PaymentService fill:#ff6b6b,color:#fff
    style UserRepository fill:#ff6b6b,color:#fff
    style NotificationService fill:#ff6b6b,color:#fff

Arrows go in every direction. PaymentService references OrderService, and OrderService also references PaymentService. Circular dependency. In this structure, touching one thing causes a chain reaction that breaks others. Even tracking who uses what becomes an arduous task.

Now let’s compare what it looks like when we build the same features with structure.

flowchart TB
    subgraph presentation["Presentation"]
        OC["OrderController"]
    end

    subgraph application["Application"]
        OS["OrderService"]
        PS["PaymentService"]
        NS["NotificationService"]
    end

    subgraph domain["Domain"]
        Order["Order"]
        Payment["Payment"]
        User["User"]
    end

    subgraph infrastructure["Infrastructure"]
        UR["UserRepository"]
        OR["OrderRepository"]
        PR["PaymentRepository"]
    end

    OC --> OS
    OS --> PS
    OS --> NS
    OS --> Order
    PS --> Payment
    NS --> User

    OS --> OR
    PS --> PR
    NS --> UR

    style presentation fill:#339af0,color:#fff
    style application fill:#51cf66,color:#fff
    style domain fill:#ffd43b,color:#000
    style infrastructure fill:#868e96,color:#fff

The direction of arrows is consistent. Top to bottom, outside to inside. You can see at a glance which class belongs to which layer, and you can predict the blast radius of modifications. This is what architecture does.

A Concrete Scenario — How an Order System Falls Apart

Concrete examples resonate more than abstract explanations. Let’s use a simple order system as an example.

Stage 1: The Quick First Version

The code below handles everything from order creation to payment and notification in a single class.

@RestController
public class OrderController {

    @Autowired private JdbcTemplate jdbc;
    @Autowired private RestTemplate restTemplate;

    @PostMapping("/orders")
    public ResponseEntity<String> createOrder(@RequestBody OrderRequest req) {
        // Check stock
        int stock = jdbc.queryForObject(
            "SELECT stock FROM products WHERE id = ?",
            Integer.class, req.getProductId()
        );
        if (stock < req.getQuantity()) {
            return ResponseEntity.badRequest().body("Out of stock");
        }

        // Deduct stock
        jdbc.update(
            "UPDATE products SET stock = stock - ? WHERE id = ?",
            req.getQuantity(), req.getProductId()
        );

        // Save order
        jdbc.update(
            "INSERT INTO orders (product_id, quantity, status) VALUES (?, ?, 'CREATED')",
            req.getProductId(), req.getQuantity()
        );

        // Call payment API
        restTemplate.postForEntity(
            "https://payment.example.com/pay",
            new PaymentRequest(req.getProductId(), req.getAmount()),
            String.class
        );

        // Send notification
        restTemplate.postForEntity(
            "https://notification.example.com/send",
            new NotificationRequest(req.getUserId(), "Order completed"),
            String.class
        );

        return ResponseEntity.ok("Order successful");
    }
}

It works. If there’s only one feature, this structure is sufficient. But then requirements start piling up.

Stage 2: Requirements Multiply

With each requirement, code accumulates in the createOrder method. A single method exceeds 200 lines, with nested if-else statements. When a new developer joins, it takes a full day just to understand what this code does.

Stage 3: Modifications Become Frightening

It’s time to modify the point accrual logic. But the point calculation depends on the payment amount, the payment amount depends on the post-coupon amount, and the coupon application is wedged in the middle of the order creation flow. To change one line, you need to understand all 200 lines, and there’s no guarantee you haven’t touched another feature.

At this point, someone says “let’s refactor.” But there are no tests. To write tests, you need a DB and external APIs. Because the code is directly coupled to infrastructure. The vicious cycle has begun.

What Is Architecture?

The term “software architecture” is used in various contexts. Microservice architecture, event-driven architecture, cloud-native architecture. The terminology alone sounds grand, but the essence is simple.

Architecture is about the important stuff. Whatever that is. — Ralph Johnson

Architecture is a set of structural decisions. How will you divide the code? How will those units communicate? In which direction will dependencies flow? How far will you allow changes to propagate? Architecture is about answering these questions in advance.

An important point: architecture deals with decisions that are hard to change later. Renaming a variable isn’t architecture. Spreading code that directly references the database across every service is an architectural decision. The latter can take weeks to undo.

Martin Fowler has described architecture as the fundamental organizational structure of a system — its components, the relationships between those components, and the implementation of principles that guide design and evolution. The key words are relationships and principles. Architecture isn’t determined by which framework you use, but by which principles govern the relationships between code.

Three Characteristics of Good Architecture

There’s no single correct architecture. But good architectures share common characteristics.

Ease of Change

The reason software exists is change. Requirements will inevitably change, and the software must be able to accommodate those changes to have value. In a good architecture, when you change the payment method, you only modify payment-related code. The order logic and notification logic don’t need to be touched.

Borrowing Robert C. Martin’s expression, software has two kinds of value: behavioral value (currently working correctly) and structural value (being able to accommodate future changes). Structural value is more important. A program that works but can’t be changed becomes useless the moment requirements shift. A program that doesn’t work but is easy to change can be fixed.

Testability

If tests are hard to write, that’s a signal of structural problems. If you need to spin up a DB, call external APIs, and start a web server just to verify business logic — it’s because the logic is coupled to infrastructure.

In a good architecture, you can pull out just the business rules and run unit tests. Here’s a simple example showing the difference.

// Poor structure — testing requires a DB
public class OrderService {
    @Autowired private JdbcTemplate jdbc;

    public boolean canOrder(Long productId, int quantity) {
        int stock = jdbc.queryForObject(
            "SELECT stock FROM products WHERE id = ?",
            Integer.class, productId
        );
        return stock >= quantity;
    }
}

// Good structure — domain logic is separated from infrastructure
public class Order {
    private final int stock;
    private final int quantity;

    public Order(int stock, int quantity) {
        this.stock = stock;
        this.quantity = quantity;
    }

    public boolean canFulfill() {
        return stock >= quantity;
    }
}

The Order class below can be tested with just new Order(10, 3).canFulfill(). No DB, no Spring Context needed.

Understandability

Reading code takes far more time than writing it. When a new team member joins, you should be able to guide them: “This is the project structure, and for this feature, look here.” Architecture serves as a map of the code.

If you can tell from the package structure alone what domains the project handles, where the business logic lives, and where the infrastructure code sits, that’s good architecture.

com.example.shop/
├── order/
│   ├── domain/        ← Core business logic
│   ├── application/   ← Use case orchestration
│   ├── adapter/       ← External connections (DB, API)
│   └── port/          ← Boundary interfaces
├── payment/
│   ├── domain/
│   ├── application/
│   └── ...
└── notification/
    └── ...

The directory structure itself is the declaration of architecture. Without opening any code, you can see that the domains of order, payment, and notification are separated, and each follows the same internal structure.

Architecture Is an Investment, Not a Cost

“Let’s build one more feature instead of spending time on architecture design.” This is something you hear often in practice. In the early stages of a project, this logic seems persuasive. When there are few features, you can move fast without structure.

But as the codebase grows, the dynamics change. In unstructured code, the cost of adding features increases steeply over time. A feature that took a day at first starts taking a week, and eventually you hear “this feature is impossible to implement with the current structure.”

In contrast, code with architectural investment takes a bit more time initially, but the incremental cost of adding features remains gentle even as they grow. This is the nature of investment: you pay costs up front and reap interest later.

Of course, over-engineering should be avoided too. Applying hexagonal architecture to a project with only three features might be wasteful. What matters is choosing an appropriate level of structure that matches the project’s scale and growth potential. The real question isn’t “to structure or not to structure” — it’s “how much to structure.”

Architectures Covered in This Series

This series spans 6 parts, examining commonly encountered software architectures one by one.

PartTitleKey Question
Part 1Why Architecture MattersWhy does unstructured code fall apart?
Part 2Layered ArchitectureThe most familiar structure — its strengths and limitations?
Part 3Hexagonal ArchitectureWhat changes when you separate inside from outside?
Part 4Clean Architecture and OnionCommonalities and differences among concentric structures?
Part 5CQRS and Event-DrivenWhat possibilities open up when you separate reads and writes?
Part 6Modular MonolithWhat can you try before microservices?

Each part can be read independently, but reading in order provides a natural progression of architectural evolution. The limitations of layered give rise to hexagonal, hexagonal principles extend into clean architecture, and when structures within a single service scale to the system level, CQRS and event-driven emerge. And seeking the best possible outcome within a monolith before going to microservices leads to the modular monolith.

A Perspective on Architecture

There’s a caution when studying architecture. Treating a specific architecture as “the answer” is dangerous. Thinking that hexagonal is always better than layered, or that microservices are superior to monoliths, leads to poor outcomes in practice.

Architecture is a product of trade-offs. Layered is simple with low learning costs but makes it easy for domains to become coupled to infrastructure. Hexagonal protects domains but increases the number of files and interfaces. CQRS maximizes read performance but raises system complexity.

The question “What is the most important quality attribute for this project?” is the starting point for architecture selection. Is the rate of change high? Is performance critical? What’s the team size? How complex is the domain? Ignoring context and just following “current trends” makes the project harder instead.

This series aims to discuss the strengths and weaknesses of each architecture in a balanced way. The goal is for readers to develop the judgment to choose the right structure for their own projects.

Key Takeaways


In the next part, we’ll cover the most familiar and widely used structure: layered architecture. That’s the Controller -> Service -> Repository structure from Spring MVC. We’ll acknowledge this structure’s strengths while examining where its limitations emerge.

-> Part 2: Layered Architecture — The Most Familiar Structure and Its Limits


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Claude Code: Frequently Used Commands
Next Post
Software Architecture Part 2 — Layered Architecture