Defining clear domain boundaries is key to keeping a software system maintainable over time. However, determining whether that original structure remains sound can be tricky, and identifying areas that require adjustment is not always obvious. One direct method is to examine the dependencies connecting the various components of your app. Taking this a step further, forensic analysis digs deeper, uncovering subtle patterns by incorporating historical data.
From an architectural standpoint, this article explores the kinds of insights forensic analysis can yield for an Angular application. To demonstrate, our open-source solution, Detective, is employed here. Its design draws on concepts from the publication Your Code as a Crime Scene, applying many of those principles in practice. The sample app analyzed in this piece is accessible via this link.
The Analyzed Example App
This application under examination is structured into two distinct domains. Additionally, a shared area provides reusable technical functionality, like logging and authentication support. The diagram below, generated by Detective from the source code, depicts how this layout is organized:

The link from ticketing to shared appears with a slightly heavier stroke, signaling a higher count of dependencies compared to what exists between booking and shared. The precise figure is revealed in the tooltip. On the surface, the setup looks reasonable: two distinct domains rely on a handful of common technical pieces. However, a closer look at these three sections surfaces additional details:

As you can observe, the number of dependencies within these domains has increased. That is a positive signal, since it indicates each domain is responsible for a cohesive set of concerns. This is called high coherence, and in an ideal world it means most changes stay contained within a single domain without leaking into others.
In a strict sense, strong coherence inside domains and weak coupling across them are just two faces of the same principle. Both aim at letting domains evolve with maximum independence. As a result, developers are not forced to hold the entire system in their heads at all times. That lowers cognitive strain and brings about better focus, fewer mistakes, and faster delivery. In the best-case scenario, the way domains are carved out also aligns with team boundaries, so each team can operate autonomously on its own domain.
Examining the Layer Structure
Yet this initial inspection also uncovers a possible issue: the feature-next-flight feature relies on feature-my-tickets. That is not inherently wrong, but it opens the door to chains of dependencies and, eventually, circular references. To avoid such problems, domains are split into multiple layers, where each layer may only depend on the ones beneath it:

In this setup, we can identify the following layers:
- feature: Includes components responsible for orchestrating use case flows. These smart components execute particular functions and, as a result, are not built for reuse.
- ui: Reusable components independent of any use case. These are often referred to as dumb or presentational components.
- domain: Contains types denoting the retrieved entities alongside Services that interact with the backend.
- util: Helper utilities, including those for authentication and logging.
The layers outlined above are in line with the concepts put forward by the Nx team, and they’ve demonstrated their value in our work, especially due to their favorable trade-off between utility and overhead.
These layers are also flexible enough to fit specific requirements. As an illustration, certain clients opt to divide the data layer into two distinct tiers: one focusing on data access, and the other supplying the related types. In such scenarios, dumb components interact only with these types, particularly because they are not expected to communicate independently with the backend.
In the example under discussion, the functionality that feature-my-tickets intends to share with feature-next-flights could be introduced through dumb components and services positioned within the ui and data layers. On the other hand, one might relax the strict layering and permit feature components within domains to reference feature components in shared. Given that communication flows in a single direction here, circular dependencies are not a concern. A further alternative is the creation of an additional layer, such as sub-feature, sitting between feature and ui.
Insights derived from evaluating the project layout frequently trigger conversations like these, leading to thoughtful choices about future development. The forensic techniques covered in the upcoming sections supply extra data and discussion points that extend well past this topic.
Forensic Analysis for Architects: A Brief Overview
The forensic code analysis techniques that Adam Tornhill proposes in his book Your Code as a Crime Scene adapt principles from criminal investigations to the study of source code. This approach leverages historical data from version control systems to pinpoint so-called hotspots within the codebase—intricate sections that see frequent modifications. Hotspots often signal architectural deficiencies that, over time, lead to instability and difficulties in maintaining the system.
Incorporating the element of time uncovers additional insights about the architecture’s evolution. One such insight is change coupling, which identifies files that are frequently modified in tandem, thereby revealing hidden interdependencies. Such knowledge proves useful when assessing how effectively the current modularization works.
A second illustration involves examining how well the team structure aligns with the module structure. When this alignment is achieved, teams can dedicate themselves to particular parts of the application, fostering greater autonomy and, consequently, higher code quality alongside a lower likelihood of faults.
Using Detective
To run an analysis of a project using Detective, go to the project’s root directory and run the commands listed below:
npm i @softarc/detective -D
npx detective
Detective requires that Git is installed and properly configured for the repository under investigation. The tool anticipates the presence of a .git directory within the folder where it is executed.
Change Coupling
The coupling examined in the preceding section stemmed directly from the relational structure between EmcaScript modules. Consequently, it can be derived from the import and export declarations found in the codebase. In contrast, the initial forensic technique I intend to introduce in this section transcends that approach and uncovers a more subtle form of dependency: Change Coupling.
The underlying principle involves pinpointing files that tend to be modified in tandem. These files share a logical affiliation, which can expose flaws in your domain separation. As an illustration, this type of coupling contradicts the previously mentioned objective that the bulk of modifications ought to reside within a single domain.
Consider this sample taken from the demo project being analyzed:

At this point, it becomes evident that the coupling between the check-in and ticketing domains is not as minimal as the earlier structural analysis suggested. Since perfect domain boundaries are unattainable, this observation can serve as a starting point for further exploration during the investigation. Adjusting the intersection of these domains could potentially enhance coherence, reduce coupling, and improve the overall separation of concerns. The bounded context concept from Domain-driven Design offers a useful framework here.
Should the discussion reveal that potential alternatives introduce more drawbacks, the choice may be to retain the existing implementation. In such a scenario, the analysis has nonetheless fostered a deeper understanding of the necessary compromises.
Hotspots as a Signal of Architectural Issues
Frequent modifications to the same file can point to underlying architectural or modularization problems. A central component might be a dependency for too many domains. When a component undergoes repeated edits for disparate reasons, it often indicates an overload of responsibilities. Research has also demonstrated that elevated code churn—frequent changes to identical files—tends to correlate with increased defect rates.
Still, there is a notable distinction between repeatedly modifying a simple file and a complex one. Consider a file storing menu item details. Each new feature could add a few lines there, resulting in substantial code churn, yet this is likely not a concern given the file's straightforward structure.
Tornhill advises weighting code churn against file complexity. He further notes that the choice of complexity metric is inconsequential. Citing a study on developer brain activity during code reading, he bluntly concludes that such metrics are weak predictors of source-code complexity. This finding probably aligns with the intuitions of most practitioners.
That study highlights vocabulary size—measured by the count of variables, functions, classes, and similar—as a factor influencing code comprehension. The length of the examined code appears to correlate at least somewhat with vocabulary size. Consequently, Tornhill employs Lines of Code as a complexity proxy in his earlier-mentioned book, whereas his product Code Scene relies on McCabe's cyclomatic complexity, a classic metric for counting execution paths through the code. Detective offers support for both approaches.
By combining churn rate with a complexity measure, the hotspot analysis yields richer context for pinpointing problematic code areas. The resulting value is known as the Hotspot Score, with higher scores flagging potentially riskier sections. No absolute threshold defines a severe problem; instead, the score acts as a prioritization guide, suggesting where closer inspection is warranted.
Displayed below is the hotspot analysis output for the demo application under review:

Because Detective prioritizes architecture, it first clusters hotspots at the module level. You can therefore immediately spot which domain or module holds the number of files exceeding the set threshold. Averages are deliberately excluded to keep critical files from being buried among many non-critical ones. Clicking any module reveals its corresponding files.
The hotspots in our demo app give little reason for concern. A total of two to three changes plus a cyclomatic complexity of 6 or less per file hardly justifies any alarm.
Team Alignment and Conway's Law
Back in 1968, computer scientist Melvin Conway observed that application structures mirror the communication patterns of their creators. A common way to illustrate Conway's Law is: when three teams collaborate on a compiler, you wind up with a compiler that has three phases.
Thus, it's wise to align team organization with the desired system design—this is called the Inverse Conway Maneuver. For instance, assigning one team to a domain encourages the low coupling wanted between domains. Single-domain focus also lightens the cognitive load.
Even when team layouts ostensibly match domain partitions, actual practice may differ. Examining commit history helps reveal whether teams genuinely concentrate on their own domains.
First, user names from the version control system need to be matched to teams. When using Detective, this means adjusting a configuration file; the relevant instructions appear in the Readme.
For our demo application, the analysis shows the correspondence between teams and domains along with the shared area:

Interestingly, the domains and the teams do not map onto each other in a clean way. In fact, the Alpha and Beta teams often seem to be doing work that really serves the Gamma team. That's worth investigating. Maybe alternative team configurations could map more naturally onto the domain boundaries — or it could be that those boundaries themselves need reconsidering.
Other explanations surface as well: teams are often organized along technical lines, or the structure is inherited from earlier arrangements. You might also see historical reasons behind the gap. Take, for example, a scenario where the Gamma team was already working long before the others arrived, and the Alpha team only later absorbed the ticketing scope. A quick way to check that is to narrow down the period you're looking at.
The shared section is not actually a coherent block; it's made up of individually reusable modules. So you'd want to run the same kind of analysis on those modules separately. That approach reveals whether any leading authors stand out. But when the modules are technical and intended for reuse, what matters more than strict team-to-code mapping is well-defined ownership. Without that, multiple contributing teams can easily introduce breaking changes.
Additionally, you can simulate the loss of knowledge that takes place when people leave, by reassigning the former members to a synthetic team:

Beyond this, the same method can be applied to hypothetical scenarios, helping you assess whether expertise should be spread more evenly across the team.
Related topic: Angular Architecture Workshop (live, hands-on, advanced)
Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!
All Details (English Workshop) | All Details (German Workshop)
From Detective to Code Scene
Unsurprisingly, the forensic analysis outlined here leaves room for enhancement. For instance, commits tied to the same feature branch or sharing a common ticket ID might be clustered. This way, change coupling remains visible even when separate commits touch different domains.
Hotspot evaluation might factor in additional elements, like how knowledge about critical regions is spread across the team. A hotspot whose source code is understood solely by a single developer carries elevated risk.
Tracing the system's evolution over time is equally valuable, as it reveals whether coupling, team alignment, and hotspot patterns have shifted favorably in recent iterations.
The commercial tool Code Scence, created by Adam Tornhill, incorporates these features and more, yielding a wider range of insights.
Critical Review
It's remarkable how forensic analysis uncovers latent patterns that align tightly with core software architecture themes—coupling, coherence, and organizational alignment.
Yet, amid the enthusiasm, it's crucial to acknowledge that this analytical approach captures only a narrow slice and can't stand in for a thorough qualitative architecture review. We must still ask whether the present architecture meets its declared goals—such as performance, security, or usability—whether key decisions around state management or authentication were deliberate, whether established patterns are applied correctly and remain sensible, and whether initial trade-off assumptions held up.
Moreover, forensic analysis fails to substitute for stakeholder discussions, including talks with product managers or developers. In every architecture review I've run, developers consistently show sharp instincts about where improvements are needed.
Derived metrics also aren't fit as objectives. Instead, they highlight subtle zones deserving closer scrutiny.
Summary
Forensic analysis techniques examine not just the current codebase but also the project's historical trajectory captured in version control systems. This yields the discovery of non-obvious patterns.
Change coupling reveals files that tend to change together, while hotspot analysis flags complex regions with high modification activity. These methods also clarify how well the team structure aligns with domain boundaries and the chosen modularization strategy.
Though this kind of analysis doesn't replace a qualitative architecture assessment, it still offers a rapid overview of critical zones that warrant inspection, questioning, and dialogue.
