DEV Community

Cover image for πŸ’‘ How to Design Better Architecture
Ivan Zaitsev
Ivan Zaitsev

Posted on

πŸ’‘ How to Design Better Architecture

Introduction

Are you about to design a new project and wondering what architecture to choose?

If your colleagues tell you that they watched a course on how to design microservices, everything will be under control - don't believe them.

Microservices because they're cool and trendy? Think twice, while they promise scalability and independence, microservices can quickly devolve into a distributed mess of tangled dependencies, network latencies, and operational headaches, making every change debugging nightmare.

Monoliths because they're more practical and straightforward? Possibly, but monoliths tend to turn into unmaintainable mess as the codebase grows, especially when multiple domains are mixed together, blurring boundaries, making every change a risky adventure across tightly coupled system.

Between these approaches lies a pragmatic middle ground. This is the modular monolith. It retains the monolith's simplicity in deployment and operations while enforcing internal structure through clear module boundaries, domain separation, and explicit interfaces.

Below are practical strategies that can help you build clean, maintainable, and scalable architecture.

Coupling and cohesion

At the heart of maintainable software are two fundamental principles such as coupling and cohesion.

Coupling describes to how much one part of a system depends on another.

Cohesion describes how closely related the responsibilities within a single module are.

Image

Low coupling: Modules interact with each other as little as possible. Changes in one module have minimal impact on others.

High cohesion: Each module has a focused, well-defined responsibility, all its internal parts work toward a single purpose.

Why it's good:

  • Easier to maintain and extend the system.

  • Changes in one module rarely break others.

  • Modules can be tested independently.

  • Encourages clear boundaries and modularity.

Image

High coupling: Modules depend heavily on each other. Changes in one module often require changes in others.

Low cohesion: Modules have mixed responsibilities, with internal parts unrelated to each other.

Why it's bad:

  • Hard to maintain or extend the system.

  • Changes in one module can break others.

  • Testing is difficult because modules cannot be isolated.

  • Leads to fragile, tightly coupled code that's hard to maintain.

Architecture Matters

Good architecture is more than a set of layers or classes. It's about creating a system that can evolve safely.

A well structured architecture:

  • Favors low coupling and high cohesion, making it easier to reason about changes.

  • Supports modularity, so that new features can be added or removed with minimal impact.

  • Promotes clear boundaries between domains and modules, reducing accidental dependencies.

  • Makes testing, maintenance, and scaling more predictable and manageable.

Without a solid architecture, even a small project can quickly turn into a fragile, unmaintainable codebase.

Monolith, Layered Architecture

The traditional layered architecture organizes classes into layers such as presentation, business logic, and data access.

Advantages:

This approach works well when an application is focused on a single domain. It enforces separation of concerns and makes responsibilities within each layer predictable.

  • Clear separation of concerns. Each layer has a well-defined responsibility, making it easier to understand.

Disadvantages:

However, when multiple domains coexist in the same project, layered architecture can lead to:

  • Low cohesion. Each layer may contain classes for multiple unrelated domains, making it harder to understand and maintain.

  • High coupling. Changes in one domain often require modifications across several layers, increasing the risk of breaking other functionality.

While layered monoliths are simple to implement initially, they can become rigid and cumbersome as the system grows.

Below is an example of layered architecture:

Image

Below is an example of layer-based packaging:

└── com.system
    └── controller
        └── order
            └── OrderController
        └── user
            └── UserController
    └── model
        └── order
            └── Order
        └── user
            └── User
    └── repository
        └── order
            └── OrderRepository
        └── user
            └── UserRepository
    └── service
        └── order
            └── OrderService
        └── user
            └── UserService
Enter fullscreen mode Exit fullscreen mode

Modular Monolith, Vertical Slices Architecture

The vertical slices architecture organizes the system into modules that represent complete features of functionality.

Advantages:

This approach works well when an application needs to evolve rapidly with multiple domains or features. It enforces high cohesion within each feature and reduces coupling between unrelated features.

  • High cohesion. Each feature encapsulates everything needed for a specific functionality, making it easy to understand, modify, and test.

  • Low coupling. Changes in one feature rarely affect others, enabling safer refactoring and independent feature evolution.

Disadvantages:

Despite its benefits, vertical slices architecture has it's own trade-offs.

  • Coupled modules. If boundaries are not enforced, modules can still become tightly coupled internally.

Below is an example of vertical slices architecture:

Image

Below is an example of feature-based packaging:

└── com.system
    └── order
        └── controller
            └── OrderController
        └── model
            └── Order
        └── service
            └── OrderService
        └── repository
            └── OrderRepository
    └── user
        └── controller
            └── UserController
        └── model
            └── User
        └── service
            └── UserService
        └── repository
            └── UserRepository
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Choosing the right architecture isn't about following trends, it's about balancing simplicity, maintainability, and growth potential.

Layered monoliths are simple and suitable for small projects with a single domain but can struggle with multiple domains.

Modular monoliths provide a pragmatic middle ground, combining structured modularity with the simplicity of a single deployment.

Microservices offer independent scaling and domain isolation but come with operational complexity that may not be justified early in a project.

Architecture is not static, it's a set of decisions that guide your system's evolution. Investing in modularity and clear boundaries early pays off exponentially as your application grows.

Top comments (0)