#book #software #architecture

This is a quick exploration view on software architecture practices which is guided by some insights from the book, “Architecture: The Hard Parts”. Here, we will cover an array of tools and techniques for analyzing and improving software architecture that are both shared in the book and discovered through my personal experiences.

JQAssistant (/ JDepend)

The authors recommend using JDepend, a tool specifically designed to analyze and visualize domain dependencies in your software. JDepend can be quite handy if you have a clear understanding of what you're after, and its integration into a build pipeline for reporting purposes is fairly straightforward. However, when starting your architectural analysis, I personally advocate for JQAssistant due to its flexibility and adaptability.

You can kickstart your journey with JQAssistant using the linked gist, which sets up a local instance and includes some prewritten queries, providing a smooth initiation. This gist covers a broad spectrum of common tasks you might encounter while scrutinizing existing software architectures.

Now, let's delve into a few example queries:

Cyclomatic Complexity for Methods and Types: These initial queries evaluate the cyclomatic complexity of methods and types in your codebase. Cyclomatic complexity gauges the number of linearly independent paths through your program's source code. An elevated complexity can hint at a method or type doing too much, possibly needing simplification or refactoring.

// cyclomatic complexity methods
match
  (a:Artifact)-[:CONTAINS]->(t:Type),
  (t)-[:DECLARES]->(m)
WHERE
  m.cyclomaticComplexity > 5 AND m.effectiveLineCount > 1
return
  m.cyclomaticComplexity as CC,a.fileName,t.name +"#" + m.signature as Method
order by
  CC desc
limit 10
// cyclomatic complexity type
match
  (a:Artifact)-[:CONTAINS]->(t:Type),
  (t)-[:DECLARES]->(m)
WHERE
  m.cyclomaticComplexity > 5 AND m.effectiveLineCount > 1
return 
  sum(m.cyclomaticComplexity) as CC, a.fileName, t.name as Type
order by
  CC desc
limit 10

Number of Cycles a Package is Involved in: This query spotlights packages entangled in circular dependencies. Such dependencies can render the architecture delicate and tricky to alter since modifications in one package could reverberate through all others in the cycle.

// Number of cycles package is involved in
match
  (a:Artifact),
  (a)-[:CONTAINS]->(p1:Package),
  (a)-[:CONTAINS]->(p2:Package),
  (p1)-[:DEPENDS_ON]->(p2),
  path=shortestPath((p2)-[:DEPENDS_ON*]->(p1))
return
  a.fileName, count(p1) as count
order by count desc

Classes with High Inheritance Hierarchies: This query helps identify classes deeply embedded in inheritance hierarchies. An excessive inheritance might suggest rigidity and fragility in your codebase, so it is often advised to favor composition over inheritance.

// Classes with high inheritance hierarchies
MATCH h = (class:Class)-[:EXTENDS*]->(super:Type)
WHERE NOT EXISTS((super)-[:EXTENDS]->())
RETURN class.fqn, length(h) AS Depth
ORDER BY Depth DESC
LIMIT 20

Cyclic Package Dependencies: Similar to the earlier query, this one also pinpoints cyclic dependencies, but it operates on the package level. These dependencies can make your codebase tough to maintain, potentially leading to unforeseen behavior and bugs.

// Cyclic package dependencies
match
  (a:Artifact),
  (a)-[:CONTAINS]->(p1:Package),
  (a)-[:CONTAINS]->(p2:Package),
  (p1)-[:DEPENDS_ON]->(p2),
  path=shortestPath((p2)-[:DEPENDS_ON*]->(p1))
return
  a.fileName, p1.fqn as package, extract(p in nodes(path) | p.fqn) as Cycle
order by
  package

@javax.persistence.Entity Annotated Classes: This query retrieves all classes annotated with @javax.persistence.Entity in your codebase, proving useful for understanding the application's data model.

// @javax.persistence.Entity annotated classes
MATCH
	(c:Class)
	- [:ANNOTATED_BY]->(annotation:Java:Annotation)
	- [:OF_TYPE]->(type: Java {fqn:'javax.persistence.Entity'})
RETURN
	c

From these examples, it's apparent that JQAssistant provides considerable value for an initial analysis of any JVM-based system's architecture. The range of queries, which are rooted in real-world use cases, provides insight into your initial focus areas:

  • Familiarizing with the structure
  • Investigating the core data model
  • Locating problematic areas
  • Identifying complex and intertwined structures which might be impeding the customer's progress.

In my experience, the first step in analyzing any system's architecture is to scrutinize the data model. As you progress, you'll find that many systems lack a formalized data model, and instead treat it more like a toolbox, only being adjusted or fixed when absolutely necessary. As you dedicate time to re-engineer a model from the customer's system, you're likely to notice logical inconsistencies and contradictory structures. These are excellent starting points to unearth past quick fixes and to reimagine an ideal data model that meets the customer's current needs.

ArchUnit

ArchUnit is a powerful library for checking the architecture of your Java code. With it, you can create and enforce architectural rules within your codebase, helping to maintain structure and consistency. For example, consider the case where we have a rule that services should not depend on each other and should only access repositories. We can enforce this using ArchUnit as follows:

@AnalyzeClasses(packages = "com.myapp")
class ArchitectureTest {

    @ArchTest
    public static final ArchRule servicesShouldNotDependOnEachOther = 
        noClasses().that().resideInAPackage("..service..")
        .should().dependOnClassesThat().resideInAPackage("..service..");

    @ArchTest
    public static final ArchRule servicesShouldOnlyAccessRepositories = 
        classes().that().resideInAPackage("..service..")
        .should().onlyAccessClassesThat().resideInAnyPackage("..repository..", "java..");
}

With this test, any violation of these architectural rules will cause a failure, allowing you to identify and address the problem quickly.

Trade-Off Analysis

Trade-off analysis is a crucial component of software architecture, helping teams to weigh different options and understand the implications of their choices. The Architecture Tradeoff Analysis Method (ATAM) by the Software Engineering Institute is a particularly helpful framework.

The ATAM involves four phases:

  1. Phase 1: Present the business drivers and architectural approaches. This involves explaining the organization's goals, the proposed architecture's capabilities, and the mapping between the two.
  2. Phase 2: Identify architectural approaches and evaluate them against the quality attribute scenarios. This phase involves exploring the architecture's response to specific scenarios and identifying any conflicts between quality attributes.
  3. Phase 3: Analyze trade-offs. Here, the scenarios are used to reason about trade-offs between different quality attributes.
  4. Phase 4: Present the results, including identified risks and non-risks, sensitivity and trade-off points, and quality attribute utility tree.

For instance, let's consider a streaming service like Netflix, which might value performance (to ensure smooth streaming) and security (to protect user data). The team might propose an architecture that uses a Content Delivery Network (CDN) to improve performance by caching content close to users.

Using ATAM, the team would evaluate this architecture against scenarios like a sudden surge in traffic (affecting performance) and a potential data breach (affecting security). While the CDN improves performance, it might introduce a security risk if not properly secured. This is a trade-off the team needs to consider, and ATAM provides a structured way to do so.

Sagas

A saga in a distributed system is a sequence of local transactions where each transaction updates data within a single service. If a local transaction fails, the saga executes compensating transactions to undo the impact of the preceding transactions.

Let's take a travel booking service as an example. The saga could involve four local transactions:

  1. Book a flight
  2. Book a hotel
  3. Book a rental car
  4. Charge the customer

If any of these transactions fails (say, the rental car is not available), the saga would execute compensating transactions (cancel the flight and hotel bookings, and ensure the customer is not charged).

Pros:

  • Maintains data consistency in distributed systems.
  • Local transactions are easier to manage than distributed transactions.

Cons:

  • Requires careful design to handle failures at any point in the saga.
  • The system may be in an inconsistent state until the saga completes, which can complicate state management and user experience.
  • Requires eventual consistency, which may not be acceptable for all applications.

The choice to use sagas will depend on the specific requirements of your system, including its need for consistency, its tolerance for latency, and its handling of failure modes.

Quantum / Quanta

In the context of software architecture, the concept of Quantum or Quanta refers to the smallest deployable unit that can function independently within a system. The principle of Quanta is inspired by physics, where a quantum is the smallest discrete amount of any physical property.

Quanta in software architecture embodies the principles of modularity, encapsulation, and autonomy, allowing for the subdivision of a larger, monolithic software system into smaller, independent units. These units can then be developed, tested, deployed, and scaled independently.

One way to understand Quanta is by comparing it to the approach in Microservices architecture. A microservice is intended to be a small, independent service that can be developed and deployed independently from other services. In a similar way, a Quantum can be seen as an independent module within a larger system.

Context and Application

The concept of Quanta is particularly relevant in scenarios where large, monolithic systems need to evolve towards more modular and flexible architectures. For example, a large enterprise application might have different modules for user management, order processing, inventory management, and billing.

The Quanta approach would involve treating each of these modules as an independent unit that can be developed, tested, deployed, and scaled independently. This approach reduces the coupling between different parts of the system, making it easier to update or replace individual components without affecting the rest of the system.

Examples

Consider an e-commerce application that originally started as a monolith, but over time has grown in size and complexity. To manage this, the development team decides to refactor the application into independent Quanta.

  • The User Management Quantum could encapsulate functionality related to user authentication and profile management. It would have its own database to manage user data, and would expose APIs for other Quanta to interact with it.
  • The Order Processing Quantum would handle everything related to orders, including placement, tracking, and cancellation. It would interact with the User Management Quantum to authenticate users and access their profiles.
  • The Inventory Management Quantum would manage the stock of items available for sale. It would interact with the Order Processing Quantum to reserve items when an order is placed and release them if the order is cancelled.
  • The Billing Quantum would handle all transactions and invoicing. It would interact with the User Management Quantum to retrieve user billing information, and with the Order Processing Quantum to get order details.

Each Quantum would be independently deployable and scalable. For example, the Order Processing Quantum could be scaled up during a sale to handle the increased load, without affecting the other Quanta.

The Quantum approach, like microservices, offers benefits in terms of modularity, scalability, and the ability to use different technologies for different parts of the system. However, it also introduces complexities in terms of data consistency, inter-Quantum communication, and deployment coordination. Careful design and planning are needed to address these challenges and effectively leverage the benefits of the Quantum approach.

Conclusion

Understanding and improving software architecture is a complex but crucial process. Tools like JQAssistant and ArchUnit, and concepts such as Trade-Off Analysis, Sagas, and Quantum / Quanta, provide powerful aids in this endeavor. However, they're only tools and methods. Successful architecture analysis and re-engineering also require careful thought, broad stakeholder involvement, and a commitment to ongoing improvement.

The mentioned book “Architecture: The Hard Parts” is a really nice pragmatic starter as well as reference for typical problems and techniques in this field.

The link has been copied!