Designing Modular Systems: Difference between revisions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search
Line 60: Line 60:


==Create Boundaries that Support Resilience==
==Create Boundaries that Support Resilience==
Components need to be designed to make it possible to quickly rebuild and recover then, in case of failure. If components are designed based on their lifecycle, take the recovery case into account.
==Create Boundaries that Support Scaling==
==Create Boundaries that Support Scaling==
==Align Boundaries to Security and Governance Concerns==
==Align Boundaries to Security and Governance Concerns==

Revision as of 00:17, 4 January 2022

External

Internal

Overview

The larger and tightly coupled a system is, the harder it is to change, and the easier it is to break. The alternative is to compose a system from small, simpler pieces. Each piece has a clearly defined interface and it is easy to understand. The component can be tested and deployed in isolation. Microservices fall into this category.

The systems should be designed for modularity. The goal of modularity is to make it easier and safer to make changes to the system. The hope is that, for a well-designed modular system, you can make a change to a component without needing to change other parts of the system. A well-designed modular system performs well on these operational performance metrics.

Designing components is the art of deciding which elements of your system to group together and which to separate. Two important characteristics of a component are coupling and cohesion. The goal of good design is to create low coupling and high cohesion.

Coupling

Coupling describes how often a change to one component requires a change to another component. Zero coupling isn't a realistic goal for two parts of a system, zero coupling means they aren't part of the same system at all. Instead we aim for low, or loose coupling. In software system engineering, especially in systems based on microservices, we aim for loose coupling. In a microservices context, we want to avoid coupling the microservice and its consumers such that a small change to the microservice itself can cause unnecessary changes to the consumer. It should be easy to replace one side of the dependency relationship without disturbing the other.

Shared modules and libraries introduce coupling between their dependents: changing the shared library (module) to accommodate a requirement of one dependent may affect the other dependents using the same library (module).

DRY and Coupling

Duplication Considered Useful

Cohesion

Cohesion describes the relationship between the elements within a component. As with coupling, the concept of cohesion relates to patterns of change. Components with high cohesion are easier to change because they are smaller, simpler and cleaner with a lower blast radius.

Cohesion is the quality of code base to have related code - code that describes a certain logical behavior - grouped together. The concept of cohesion is reinforced by the Single Responsibility Principle espoused by Robert C. Martin. Any given component should have responsibility for one thing, so it is focused, so its contents are cohesive. The component "should fit in your head".

In software system engineering, especially in systems based on microservices, we aim for high cohesion. High cohesion allows us, if we want to change behavior, to change it in only one place, and then deliver the change via a simpler release process than required if the behavior were to be changed in a lot of places. Making changes in many places is slower, and the consequent release process is riskier.

High cohesion software systems are designed by finding the right boundaries of our problem domain. Right boundaries ensure that related behavior resides in one place, and the communication with other boundaries is as loose as possible. All decisions about what changes are allowed on the state maintained within the bounded context must be taken inside the bounded context. If the decision about what changes are allowed leak out, we are losing cohesion.

Kent Beck's Rules of Simple Design

Beck Design Rules by Martin Fowler

A well designed system should:

  • Pass its tests (do what is supposed to do).
  • Reveal its intentions (be clear and easy to understand).
  • Have no duplication. Don't Repeat Yourself.
  • Include the fewest elements.

Design Components around Domain Concepts, not Technical Ones

Shared components create coupling between their dependents, and components based on technical concepts introduce artificial coupling.

Define Interfaces between Components

This is a direct consequence of the Law of Demeter.

Circular Dependencies

Components should not define circular dependencies.

Use Testing to Drive Design Decisions

Test Driven Development (TDD) and automated testing drive better design. Loosely coupled, high cohesion component systems are naturally automatically testable. It's easier to build and use mocks for a component with cleaner, simpler interfaces.

Boundaries between Components

Find natural places to draw boundaries between parts of your system, where you can create clean integration points. Components such defined should be exposed externally via interfaces. There are several strategies that can be used:

Align Boundaries with Natural Change Patterns

For an existing system, insight into what things change together is given by examining historical changes: code commits, for example. By understanding what components tens to change together as part of a single commit, or closely related commits across components, you can find patterns that suggest how to refactor the code for more cohesion and less coupling. Good sources of information are artifacts around higher levels or work: tickets, stories and projects. However, you should optimize for small, frequent changes.

Align Boundaries with Component Life Cycles

Understand how components will be created, modified and destroyed. In case of infrastructure, organizing infrastructure resources in deployable components according to their life cycle can simplify their management and may help saving costs (components not used often can be shut down when not used). Moreover, optimizing infrastructure components for life cycle is useful for automated testing in pipelines.

Align Boundaries with Organizational Structures

Alignment of system architecture to organizational structure is is worth considering especially for infrastructure. Aligning component boundaries to the organizational structures is an acknowledgment of Conway's Law. Avoid designing components that multiple teams need to make changes on. Or, consider restructuring teams to reflect architectural boundaries according to the inverse Conway maneuver. The goal is to make the change less disruptive: rather than negotiating a single change window with all of the teams using a shared instance, separate windows can be negotiated for each team.

Create Boundaries that Support Resilience

Components need to be designed to make it possible to quickly rebuild and recover then, in case of failure. If components are designed based on their lifecycle, take the recovery case into account.

Create Boundaries that Support Scaling

Align Boundaries to Security and Governance Concerns