My Book Notes: Software Architecture: The Hard Parts (Part 1)
Book Review
Experienced authors of the book tells us how architecture decisions made with proper trade-off analysis over conversion of entangled monolithic project to microservice architecture. Throughout the book you will learn there is no silver bullet for architecture. When the actors of the project in book encounters a problem, they overhelm the problems with good communication, documentation and proper analysis. I really appreciate the authors since they’ve written this book, because I’ve learn too many things about how I can turn things to my side while everything was going bad.
Chapter 1. What Happens When There are no Best-Practises?
Don’t try to find the best design in software architecture; instead, strive for the least worst combination of trade-offs.
Fitness functions validates the structure of architecture not domain criteria; unit tests are the opposite.
Software architecture is the stuff you can’t Google answers for.
Chapter 2. Discerning Coupling in Software Architecture
To discerning coupling problems architects must analyze trade-offs;
- Find what parts are entangled together.
- Analyze how they are coupled to one another.
- Assess trade-offs by determining the impact of change on interdependent systems.
Architecture Quantum (Plural: Quanta)
An Architecture Quantum is an independent application which is deployable, cohesive. For example a well-structured microservice is an architecture quantum.
Static Coupling
Static coupling is dependency between classess, functions, databases etc.
Static coupling analyzes operational dependencies.
Dynamic Coupling
Dynamic Coupling is calling other services at the runtime.
Dynamic coupling analyzes communicational dependencies.
High Functional Cohesion
It is equal to Bounded-Context in Domain Driven Design. All parts of an architecture quantum are related to each other.
💡 Quanta number of a project is how many coupling points exist in the architecture.
High Static Coupling
It implies that the elements inside the architecture quantum are tightly wired together. For example: common contracts between APIs, shared databases.
Dynamic Quantum Coupling
Synchronous communication between 2 services might be an example for the definition.
Chapter 3. Architectural Modularity
Maintainability
Maintainability is about the ease of adding, removing or updating part of software application.
Maintainability metric which was invented by Alexander von Zitzewitz.
Testability
It is defined as the ease of testing (usually implemented through automated tests).
Deployability
It is not just about the ease of deployability, it is also about the frequency and risk of deployment. The frequency of deployment should be high.
Scalability
Scalability is affording the ability of high number of users’ traffic.
Elasticity
Elasticity is affording the ability of changing frequently number of users’ traffic
Availability / Fault Tolerance
Availability is the ability of one part of an application to be broken, but other parts of it work.
Chapter 4. Architectural Decomposition
Component-based decomposition
It is an extraction approach that applies various refactoring patterns for extracting components which is the logical building blocks of an application.
💡 When migrating a monolithic applications to microservices, moving to service based architecture should be considered since the database would be the same and the architects could focused on domains separation.
Tactical Forking
This approach involves making replicas of an application and chipping away at the unwanted parts to form services. Tactical forking can be achieved with this steps:
- Clone the repository
- Assign the repos to the responsible teams
- Each team should delete the unused parts that don’t break the system.
To detect what approach can be used, code base structure of the application should be inspected. If the components of the codebase are tightly coupled, tactical forking should be chosen.
Benefits of Tactical Forking
- Teams can start working right away on their services.
- Deleting code easier than extracting components from a chaotic codebase
Shortcomings of Tactical Forking
- The resulting services will still contain large amount of legacy code.
- Inconsistencies may occur between shared code and shared component files.
Chapter 5. Component-Based Decomposition Patterns
5.1 Identify and Size Components Pattern
The first step of migration from monolithic to distributed.
Component Size Percent: The amount of source code of a component ratio according to the project size.
Component Statements: The count of lines of source code in a component.
Files: Total number of source code files in a component.
For example in a 10-component project, component size percentage per component should not exceed 25%.
5.2 Gather Common Domain Components Pattern
Let’s say that we have an SMTPConnection class which is inherited from 5 different classes in 5 different components. All 5 classes are used for e-mail notification, so they should be gathered in a separated service, a shared library or a component.
💡 The coupling metric should be calculated for the newcomer block of code.
5.3 Flatten Components Pattern
Component: A collection of classes grouped within a leaf node namespace.
Root Namespace: A namespace node that has been extended by another namespace node. For example, given the namespaces ss.survey
and ss.survey.templates
, ss.survey
would be considered a root namespace because it is extended by .templates
. Root namespaces are also sometimes referred to as subdomain.
Orphaned Classes: An orphaned class is inside of root namespace.
💡 Flattening a component is removing orphaned classes. Thus source codes reside only in leaf nodes.
5.4 Determine Component Dependencies Pattern
The purpose of this pattern is to analyze the incoming and outgoing dependencies (coupling) between components. To break apart a monolithic application into a distributed one, logical coupling between components (not the functional ones, such as common utility functions) should be low.
5.5 Create Component Domains Pattern
This pattern is to logically group components together in order to make the component groups separate services.
5.6 Create Domain Services Pattern
Once components have been properly sized, flattened and grouped into domains, those domains can then be moved to separately deployed domain services aka service based architecture.
Chapter 6. Pulling Apart Operation Data
Data Distintegrators
Drivers that justify breaking apart data.
Change control
How many services are impacted by a database table change?
Connection management
Can my database handle the connections needed from multiple distributed services? Since the app services use the same database in service based architecture, the connection count quota should be assigned for each service in order to solve the connections limit exceed.
Scalability
Can the database scale to meet the demands of the services accessing it?
Fault tolerance
How many services are impacted by a database crash or maintenance downtime?
Architectural quantum
Is a single shared database forcing me into an undesirable single architecture quantum?
Database type optimization
Can I optimize my data by using multiple database types?
Data Integrators
Drivers that justify keeping data together.
Data relationships
Are there foreign keys, triggers, or views that form close relationships between the tables?
Database transactions
Is a single transactional unit of work necessary to ensure data integrity and consistency?
Decomposing Monolithic Data
Data Domain: it is a collection of coupled database artifacts such as tables, views, foreign keys and triggers.
Step 1: Analyze Database and Create Data Domains
Identify specific domain groupings within the database.
Step 2: Assign Tables to Data Domains
When tables belonging to different data domains are tightly coupled and related to one another, data domains must necessarily be combined in a broader bounded context where multiple services own a specific data domain.
Step 3: Separate Database Connections to Data Domains
Schema connections should be separated for each independent service, which has been defined the previous step.
Step 4: Move Schemas to Separate Database Servers
There are two ways to achieve that;
Backup and Restore
This requires downtime.
Replicate
This avoids downtime but it requires more work to setup the replication and manage increased connection.
Step 5: Switch Over to Independent Database Servers
Connections between service and database should be switched to the new database servers.
Selecting a Database Type
Selecting a database requires knowing characteristic features of them;
- Ease-of-learning curve
- Ease of data modeling
- Scalability/throughput: Can the database scale horizontally, vertically, or both?
- Availability/partition tolerance
- Consistency
Relational Databases
Relational databases favor consistency over availability and partition tolerance.
PostgreSQL, MySQL
Key-Value Databases
NoSQL
Redis, Memcache, Amazon DynamoDB
Document Databases
NoSQL
MongoDB, Amazon DocumentDB
Column Family Databases
NoSQL
Apache Cassandra, Scylla
Graph Databases
NoSQL
Neo4j
NewSQL Databases
Mixed of NoSQL and traditional RDBMS features.
CockroachDB, ClustrixDB
Chapter 7. Service Granularity
Granularity refers to how fine-grained or coarse-grained your microservices are, that is, how much functionality and data they encapsulate.
Modularity refers to how well-defined and cohesive your microservices are, that is, how clearly they separate their concerns and responsibilities.
Granularity Disintegrators
Service Scope and Function
Is the service doing too many unrelated things?
Check cohesion of the service and ofcourse comply the Sinle Responsibility Principle.
Code Volatility
Are changes isolated to only one part of the service?
Check code changing frequency for each part of a service.
Scalability and Throughput
Do parts of the service need to scale differently?
Fault Tolerance
Are the errors taht cause critical functions to fail within the service?
Security
Dome some parts of the service need higher security levels than others?
Extensibility
Is the service always expanding to add new contexts?
Granularity Integrators
Database Transactions
Is an ACID transaction required between separate services?
Workflow and Choreography
Do services need to talk to one another?
Shared Code
Do services need to share code with another one?
Except logging, authentication, monitoring etc.
Data Relationships
Can services use the separated data?
Chapter 8. Reuse Patterns
Code Replication
Copying the code blocks to each service.
Tradeoffs
Shared Library
Versioned shared libraries like dll, jar or package.
Tradeoffs
Shared Service
A shared service which can be deployable itself when a change must be done without affecting other services. But a simple change in a shared service might bring down most of the services that use it. To fix this problem, api versioning can be used. But another problem raises after versioning, which is changing request endpoint versions in the services that use the shared service.
Tradeoffs
It can be used when multiple languages are used in different services.
Sidecars and Service Mesh
Inspired from Hexagonal architecture principle (ports and adapters). It is architectural equivalent to the Decorator Design Pattern. Consolidating operational coupling not domain coupling. Sidecar is a separate application running with its connected service.
It can be used when multiple languages are used in different services.
Orthogonal Coupling
Two distinct purposes that must still intersect to form a complete solution. For example monitoring is necessary but independent from domain behaviour.
Tradeoffs