Abstract

Uncle Bob outlines what he has learned over the years when it comes to architecture on a systems and project level. The goal of architecture is to reduce the long-term cost of a project by delaying decisions and maintaining as much flexibility as possible. A special team does not accomplish this goal but architects who are the best programmers. Through the pragmatic application of principles and architectural methods, one can make a project easier and more cost effective.

Principles

Lessons

Preface

Chapter 1: What is Architecture?

Chapter 2: A Tale of Two Values

Chapter 3: Paradigm Overview

Chapter 4: Structured Programming

Chapter 5: Object Oriented

Chapter 6: Functional

Part III: Design Principles

Chapter 7: Single Responsibility Principle

Chapter 8: The Open Closed Principle

Chapter 9: Liskov Substitution Principle (LSP)

The LSP in spirit says that objects should be substitutable for their base type.

A famous example of this is the square & rectangle problem. A square is a rectangle with the same sides that must change together. Consequently, a square is not a proper subtype of a rectangle.

But the LSP pertains not just to interfaces and implementations but also to architecture. Similarly, REST services must conform to the principle.

Violations of substitutability can cause a system’s architecture to become polluted and unmaintainable.

No programmer worth their salt would use a conditional (if) to check for specifics about a type.

Chapter 10: Interface Segregation Principle

Whenever possible, it is best to not depend on modules you don’t need.

Use and declaration statements in source files are what create dependencies that force recompilation and deployment. Dynamically typed languages create systems that are, in general, more flexible and less tightly coupled.

Chapter 11: Dependency Inversion Principle (DIP)

The DIP basically states that high-level modules should not depend on low-level modules and both should depend upon abstractions.

However, in true Pirates of the Carribean fashion, it is a guideline and not a rule since we often depend on a languages standard library and on operating system utilities. These tend to be very stable. So we can see an extension of the rule is that stable architecting avoid depending on volatile concretions.

In addition, it is best not to derive from volatile concrete classes.

When you need to create a volatile concrete object us an abstract factory. DIP violations cannot be removed entirely. With this principle, dependencies are inverted against the flow of control. This rule also applies to dynamically typed languages.

Component Principle IV

Chapter 12: Components

Components are the units of deployment, the smallest entities that can be deployed as part of a system.

Murphy’s law of program size: “programs grow to fill all available compile and link time.” Goes over the history of components.

Chapter 13: Component Cohesion

Three principles of component cohesion are fighting one another. Early on the CCP is more important than the REP. The appropriate spot in the tension triangle depends on the current state of the project.

The REP and the CCP tend to make components larger while CRP tends to make them smaller.

REP - the granule of reuse is the granule of release. Must be a cohesive group. Should be releasable together. Violations simply don’t make sense.

CCP - group classes that change for the same reasons and same times. Maintainability more important than reusability. Associated with OCP and SRP. 100% closer is not attainable.

CRP - classes and modules that tend to be reused together belong in the same component. Want to make sure to depend on every class in the component. Don’t depend on things you don’t need. Generic version of the ISP.

Chapter 14: Acyclic Dependencies Principle (ADP)

States that there should be no cycles in the component dependency graph. Too often the work of devs is dependent on the work of other devs. When it comes time to integrate their work it is a mess. The build is often broken. One solution is the weekly build but this runs into problems as the project grows since it starts taking longer and longer to integrate and deploy.

Another solution is to eliminate dependency cycles altogether by partitioning the environment into releasable components. Coupled with versioning this is a very workable scheme since integration happens in small in small increments. No single point in time.

In order for incremental deployments to work there must be no cycles or cyclic dependencies in the code - it must be a directed graph. To release the whole system, proceed from the bottom up.

Breaking a cycle - to break a cycle, either apply the DIP or create a new component.

Top down design - a component structure cannot be designed from the top down. Dependency diagrams have very little to do with describing function. They describe the buildability and maintainability. The dependency structure grows with the design of the system.

Depend in the direction of stability. Stability is related to the amount of work required to make a change.

Stable components should not depend on volatile components. Components that are depended on by lots of other components are difficult to change.

Stability metrics

Lack of dependents gives a component no reason not to change. Not all components should be stable. Changeable (volatile) components on tob and stable components on the bottom.

Abstract components - in statically typed languages it is a useful practice to create components that contain nothing but interfaces. Those components are very stable.

Stable Abstractions Principle (SAP) - A component should be as abstract as it is stable. Interfaces and abstract classes. High-level policies should be placed into stable components. Unstable components should be concrete.

SAP + SDP = DIP for components

Dependencies run in the direction of abstraction. A component can be partially abstract and partially stable.

Measuring abstraction - ratio of abstractions to concretions in the component.
* NC: number of classes * NA: number of abstractions * A = NA / NC

Abstraction graph - Main sequence is the place to be. 0,0+1,1 are not the places to be since it’s rather painful or useless. 0,0 is fine for non-volatile components such as the SPL or OS. 1,1 is abstract with no dependents. Volatile components should be kept on the main sequence.

Distance from main sequence: D = |A+I-1|

Where I = stability + A = abstractions D = Distance

Chapter 15: What is architecture

The job of the Architect is to maximize productivity and leave as many options open for as long as possible and minimize the lifetime cost of the system. They make it so that the system is easy to understand, develop, maintain, and deploy.

However, they don’t just have their heads in the clouds - they’re programmers, the best. They can’t do their job properly if they’re not experiencing the problems they’re creating. The harder to develop the shorter the life expectancy. The higher the cost of deployment, the less useful the system is. Should be deployable with a single action.

Architectures that impede dev or deployment are more costly than those that impede operations, maintenance.

Two types of value: behaviour and structure.

Software systems contain two major components: policy and details.

Policy is the most important. Details are irrelevant to the policy. Decisions about details should be deferred. A good architect maximizes the decisions not made. High-levl policy should be against the interface to the outside world. Should not care about dependency resolution.

The longer you wait the more info you have. Good architects carefully separate details from policy and then decouple policy from the details.

Chapter 16: Independence

Any organization that design a system will produce a design whose structure is a copy of the organization’s communications structure. The decoupling made of a system is likely to change with time. If the architecture is right there it can progress from a monolith to independently deployable units to micro-services.

But one typically shouldn’t start out with microservices since they are expensive and a waste of effort. Push the decoupling to the point where a service could be formed. Three decoupling modes: source, deployment, and service level.

Resist compulsive elimination of duplication. Things that change at different rates and for different reasons are not true duplicates.

Good architecture leaves options open. Decoupling mode is one of those. Use the SRP and the CCP to separate things that change for different reasons. If you do that then it will be easy to add new use cases without interferring with old ones.

Use cases can be used as natural dividers of a system. Keep them separate down the vertical height of the system. There are few behavioural options that a system can leave open.

Chapter 17: Boundaries

Drawing lines is the art of drawing boundaries. Boundaries define a system. The SRP tells us where to put the boundaries. First you partition the system into components and then you arrange those components according to the DIP and the SAP. Doing so ensures that dependencies flow or point from the lower-level to the higher-level abstractions.

The GUI doesn’t matter to the business rules. The DB is a tool that the business rules can use indirectly. Drawing boundaries helps delay and defer decisions. Try to defer decisions into non-existence. Be weary of flushing hours down the SOA vortex needlessly. Early boundaries drawn to defer decision. Coupling saps the line and energy of the development team especially coupling to premature decisions. Making premature decisions multiplies dev efforts.

Chapter 18: Boundary Anatomy

The architecture of a system is defined by components and boudaries. There are several boundary strategies including the monolith, deployment components, local processes, and services.

Boundaries are about safeguarding against change. The monolith is the simplest form even though the boundaries are not visible during deployment. Without OO you’d have to dangerously rely upon function pointers to acheive decoupling dynamic polymorphism used to invert dependencies against the flow of control. Dependencies cross boundaries toward the higher level components. Communication fast though chatty across decoupled boundaries.

A system broken up into deployable components is essentially the same as a monolith except for being broken up and deployed using multiple files. Communications are still very chatty. A stronger physical boundary is the local process. Segregation strategy is the same. Communications involve the OS and other systems and should be limited.

The strongest bounder is a service which can be local or remote. Assume communications take place over a network and slow. Avoid chatting where possible.

Chapter 19: Policy & Level

Policies determine how a program’s inputs are transformed into outputs. Programs are essentially just statements of policy. A major part of architecture is grouping policies based on how they change (components) ensuring that low-level components depend on high-level components. Components should often form a directed acyclic graph. Code dependencies should be decoupled from data flow and coupled to level. A “level” is the distance from the inputs and the outputs. Farther from in/out = higher level.

Separating policies and ensuring that they are at the right level reduces the impact of change. Lower-level policies tend to change frequently but for trivial reasons. Policy theory touches several such principles: SRP, OCP, CCP, DIP, SDP, SAP.

Chapter 20: Business Rules

Business rules are rules or procedures that make or save the business money. They are made up of critical business rules and critical busiess data. Both would exist without the system being automated.

An Entity embodies critical business rules operating on critical business data which it contains or has easy access to. An entity is pure business. A use case defines and constrains a system in a way that would not be done in a normal environ. They define an automated system. Don’t describe how the system appears to the user. Is an object. Entities have no knowledge of the use cases that control them. Entities are higher level and use cases lower level. A use case is an object. They do not describe how the system appears. Use cases accept simple request data structures or input and returns a simple response data structure as output. These are not the framework response and request interfaces.

Do not allow these data structures to contain references to entity objects. Avoid trying them in any way to anything that violates the CCP and the SRP.

Chapter 21: Screaming Architecture

Your architecture should tell others about the system, not about the framework you’re using. Frameworks are tools to be used not architectures to be comformed to. Good architectures are centered on use cases. They should be able to be unit tested without the framework. Entity objects should be plain objects. Your architecture should be as ignorant as possible about how it is delivered. Same with frameworks and databases. Those decisions should be deferred. What does our architecture scream?

Chapter 22: The Dependency Rule

There are several different architectures, hexogonal, DCI, BCE, and Uncle Bob’s Clean Architecture. But they all acheive the same objective: separation of concerns by dividing a system into layers. They tend to have the following characteristics:

Dependency rule: source code dependencies must point only inward toward higher-level policies.

Layers:

Frameworks + drivers -> interface adapters -> use cases -> entities

You may need more than 4 layers depending on your system.

Entities encapsulate enterprise-wide critical business rules.

Use cases are application specific business rules. Changes in this layer should not efect the entities.

Interface adapters convert data from the most convenient for use cases and entities to the format most convenient for some external agency. SQL restricted to this layer.

Frameworks and drivers is the outermost layer. Generally you don’t write much code in this layer.

Resolve contradictions by using the DIP. No name in an outer circle can be mentioned in an inner circle. Data that crosses boundaries should be DTOs and or structs but not entities or database rows. By separating a system into layers, you make a system that is testable.