Skip to main content

Prime Radiant: What and how we measure our software

· 8 min read
Maxim Verbruggen
Maxim Verbruggen
NS Analyst

Writing good code at NSX is easy: You model a workflow, choose a fitting task name, and start writing a simple business calculation (e.g. summing your invoice lines to an invoice total). Your code is clean and well-structured, and you build on the foundation of a qualitative stack.

Thanks to the expand and build in the micro-radiant, our expansion framework, automagically, delivers a whole set of code. It supplies you with modern user interfaces, it manages data persistency, exposes an API and much more.

But once in a while, a more daunting task might require you to extend the stack with unsupported functionality... Maybe your client wants to export his data in a proprietary report structure, or you need to integrate with an obscure authentication framework, or maybe your client wants all action buttons validated with a PIN code.

To ensure these custom additions (and for that matter all code) remain maintainable, we track each project with automated measures. In this post, we explain how we measure each project and how these measures help you to keep your project in good shape.

Gathering data throughout the build pipeline

At NSX, we provide each project with a default build pipeline. The pipeline integrates with a whole set of tools that capture dependency health, calculate test coverage, define code metrics and analyze your code structure.

The different stages in the pipeline might call an external system or produce a report artifact. All this information is afterwards aggregated and linked to your application in our central intelligence platform: Prime Radiant.

A deeper look into our pipeline

The lifecycle of a pipeline consists of several stages that produce measures:

  • Build: Executes the mvn package command. This uses the CycloneDX plugin to generate a bom.json (Bill of Materials).
  • Report: Pushes the bom.json to DependencyTrack for security analysis. It also triggers SonarQube for code quality and gets a sonar-xml.report with high-level statistics back.
  • NS-Audit: Calls our custom NS Audit Tool that validates code on NS principle violations and generates an audit.json result.
  • Analyze: This is where the Project Analyzer comes in. It scrapes the source code, counts files, and interprets the models. It reads the bom.json, audit.json, and sonar-report.xml and pushes everything to Prime Radiant.

Project Analyzer

The analyzer is a central Java application that we developed in-house. It is responsible for parsing all measure data available in the pipeline, calculating additional measures, and sending everything to Prime Radiant.

Some of the measures that it calculates include:

  • Codesize: Measuring the total footprint of the codebase.
  • Model size: Reading the application model and determining the amount of model elements of each type (Data Elements, Tasks, Flows, etc.).
  • Detecting extensions and insertions: Identifying specific points in the source code where you added custom code or extended the framework.
  • Overlays: Looking for overlays to see where the generated templates are overwritten.

While the project analyzer provides most of the measure data, some project-independent data is gathered by the Prime Radiant directly. One of such example is pulling vulnerability data from an open platform to enrich our internal dependency health measures.

What do we measure?

For applications, we split our measures into two main categories: Size Metrics and Quality Metrics.

Measures that are shared with other software artifact types are linked to the generic element AssemblyUnitBatch, application-specific measures are linked to ApplicationVersion.

Plugins, Expansion Resources and libraries have their own set of measures which are out of scope of this article.

Size Metrics

Size metrics describe the size of your codebase and model. These are useful for:

  1. Tracking growth over time: See how your application size is evolving (e.g. How many new data elements were introduced in the last year to monitor application's growth.)
  2. Defining "peer" applications: Find similar projects to use as a benchmark. (e.g., If an application with the same amount of data elements has three times the workflows, maybe you should model your flows more granularly).
  3. Normalizing quality metrics: Use size to put quality in context. (e.g. How much custom code do you have per data element, and does it remain stable as you grow?).

Quality Metrics

Quality metrics check how solid the code is. We group these measures into three areas: Custom code, quality and dependencies.

1. Custom Code & Overlays

These measures quantify how an application extends the generated stack. We monitor these to identify projects that are becoming overly customized, which increases the complexity of future upgrades and maintenance.

  • CustomCodeMeasure: We track nrOfExtensions (files) and nrOfInsertions (files with code added within anchors). We also monitor totalSizeExtensions and totalSizeInsertions in bytes to assess the volume of custom implementation logic.

  • OverlayMeasure: This tracks overrides of standard files via nrOfOverlays and totalSizeOverlays. A high overlay count often indicates that the core framework lacks a specific feature, leading to project-specific workarounds.

2. Code Quality (SonarQube)

Code quality metrics are primarily derived from SonarQube, focusing on three technical domains: Reliability ( functional correctness), Maintainability (ease of modification), and Security (resistance to exploits).

Issue counts

The aggregated issue counts help you validate code stability over time and can be used to benchmark your application against others in the Prime Radiant

  • maintainabilityIssues: The count of technical patterns that increase complexity and make the codebase more difficult to modify.
  • reliabilityIssues: The count of logic errors that increase the probability of runtime failures.
  • securityIssues: The count of security flaws that represent potential points of exploitation.
  • Severity Breakdown: Issues are categorized as lowSeverityIssues, mediumSeverityIssues, or highSeverityIssues based on internal rankings of SonarQube.

Ratings

A rating tries to quantify the quality of your application in a single number (based on all detected issues). Any rating deviating from the best grade (1) might be a good starting point to start improving your application.

  • debt: The estimated effort (in minutes) required to remediate all open maintainability issues.
  • maintainabilityRating: A grade (1-5) based on the Debt Ratio (the cost to fix the code vs. the cost to rewrite it).
  • reliabilityRating: A grade (1-5) derived from the severity of the most critical open bug.
  • securityRating: A grade (1-5) based on the severity of the most critical open vulnerability.

Code complexity

  • duplication: The percentage of repeated code blocks.
  • cyclomaticComplexity: A mathematical measure of the number of linearly independent paths through the code. It reflects how difficult the code is to unit test.
  • cognitiveComplexity: A measure of how difficult the code is to understand for a human. It penalizes nested loops and complex logic structures.

3. Dependency measures

Managing dependencies is an important factor in application quality. Every external library added increases the maintenance surface of the application and introduces external risks.

At NSX, we use Renovate Bot to automate update tracking. However, updates still require careful (and manual ) management. Breaking changes may require refactors, and version updates may unintentionally impact internal implementations.

Dependencies also extend the application's attack surface. Vulnerabilities within third-party libraries become direct security risks for the project.

We monitor:

  • nrVulnerabilities: The total vulnerability count, categorized from nrCritical to nrInfo. The urgency category is based on public CVE scores.
  • nrOutdatedVersions: The number of dependencies currently behind the latest (known) release.

What's next?

Analyzing our projects and storing this information in a structured way is just the first step. As a chief engineer, it is up to you to watch over your application's health.

The Prime Radiant offers you a view to check, from time to time, how your application is doing. In the daily run of a project, it is surprisingly easy to forget best practices, choose a quick shortcut, or leave a "dirty patch" that never gets fixed.

Tracking these measures allows you to reflect on this evolution and make time for a necessary refactor in a specific component or layer (i.e. if you went a little wild with UI customizations).

To help you with this, the Prime Radiant team is working on:

  1. Automated notifications: Get alerts via mail, Slack, or build failures when your project crosses defined measure thresholds.
  2. Dashboard views: Compare your application with other projects to detect trends or evolutions that might be less than ideal.

Below is a dashboard preview showing an application with an unusually high number of extensions compared to its data elements:

Amount of dataelementsAmount of dataelements Amount of extensionsAmount of extensions

Prime Radiant: Towards an integrated software factory control system

· 5 min read
Maxim Verbruggen
Maxim Verbruggen
NS Analyst

Introducing the Prime Radiant: An integrated software factory control system

Software data is scattered across multiple tools (such as Git, Jira, Jenkins, Maven, SonarQube, etc.), making it challenging to have an integrated overview of one (or many) applications. Next to this, software systems become more pervasive to manage and controlling the end-to-end production process of every application in your organization can be a daunting task. Therefore, it is our goal at NSX to build the foundations for a true software factory, to manage and control the building and assembly of software systems.

The Prime Radiant provides a single analysis platform and control layer to optimize NSX factory's operations and output. (i.e. application and expander developments) It works by automatically collecting, aggregating, and integrating information from our different DevOps tools.

The result is a unified view of the development lifecycle across all applications. This not only supports data analysis but also offers the potential to control our factory: adding new expansion resources, patching vulnerabilities, or deploying new application versions, all directed from the Prime Radiant.

In the next sections, we'll describe the core functionality of the Prime Radiant. For each functionality we show a conceptual data model that contains the relevant data concepts.

Please note that all diagrams are showing a simplified version of the actual implementation

Managing Assembly Units

A fundamental concern of the Prime Radiant is capturing and identifying all software created within the NSX factory. An Assembly Unit defines every piece of software or technology produced, allowing us to manage the factory’s artifacts comprehensively.

An Assembly Unit can be an Application (like a JEE System) , an Expansion Resource (e.g. Angular front-end expander) , a Library (providing basic utilities) , or a Developer Tool (plugins and command-line tools), or a unit combining any of the former.

For every unit, we keep track of the source code by mapping its associated Repository.

Tracking Versions

To analyze and optimize applications over time, we must track changes and their impact. Versioning is fundamental to tracking software changes. As a result, each software element is mapped to a specific versioned entity, like an AssemblyUnitBatch or ApplicationVersion. This is crucial because it allows us to link dependencies, metrics, and vulnerabilities to its specific state at a certain moment.

Monitoring metrics

A core function of a control system is tracking data over time. The prime-radiant logs crucial metrics for applications, expansion resources, libraries etc. We have defined a core set of shared metrics (such as Test Coverage and Code Quality) that apply to all Assembly Units. Additionally, we implement more specific metrics tailored to the unique requirements of each Assembly Unit type.

For example, for the unit type Applications, we monitor the amount and size of custom code (extensions/insertions), relative to the generated software to ensure maintainability and evolvability.

Deployments

Deploying applications and managing the underlying infrastructure is a part of every development lifecycle. The Prime Radiant tracks Application Deployments to record information such as server specifications, OS versions, cloud provider type, and domains.

Build pipelines

Each version of an Assembly Unit is traced back to a specific build produced by a pipeline. The pipelines are defined with PipelineTargets (steps) that make use of various BuildTechnologies, and ultimately produce versioned Resources (libraries, archives, or executables).

Modeling business requirements

Creating software is not an end goal; it's done to fulfill specific business needs, making business context paramount. In the Prime Radiant, this context is modeled and captured.

Every Application, is linked to a Domain (e.g. car rental) and part of a larger Digital Platform (such as 'euRent platform').

This platform is described using Business Capabilities (e.g., invoicing, scheduling, maintenance) and Functional Requirements (e.g., canceling a booking).

This approach allows the software manufacturing control system to link development efforts directly to business value.

December 2023 R&D update

· 18 min read
Frédéric Hannes
Frédéric Hannes
R&D Engineer
Jan Hardy
Jan Hardy
R&D Engineer
Jorren Hendriks
Jorren Hendriks
R&D Engineer
Koen De Cock
Koen De Cock
R&D Engineer

On the 7th of December, we presented an overview of some of the major developments over the last half year in the NSX R&D team. This post is a guiding article for the presentation, which summarizes the notable topics that we touched on. We now also include an overview of planned development over the course of the coming half year.

Download the presentation

June 2023 R&D update

· 5 min read
Frédéric Hannes
Frédéric Hannes
R&D Engineer
Jorren Hendriks
Jorren Hendriks
R&D Engineer
Koen De Cock
Koen De Cock
R&D Engineer

On the 15th of June, we presented an overview of some of the major developments over the last half year in the NSX R&D team. This post is a guiding article for the presentation, which summarizes the notable topics that we touched on.

Download the presentation

On the Use of Inheritance

· 7 min read
Herwig Mannaert
Herwig Mannaert
Founder

The technique of inheritance was introduced as an essential part of the object-oriented programming paradigm, aiming to deliver more anthropomorphic code by mimicking the concept of ontological refinement. Just like a jet fighter is a special type of an airplane, or a bird is a special type of animal, and a sparrow is a special type of bird, inheritance was created to define classes as refinements of other classes. Such a subclass would inherit the member variables and methods of a superclass, and extend it with more specific variables and methods for that particular refinement. The notion of ontological refinement is strongly related to taxonomy, i.e., the practice and science of the classification of things or concepts, including the underlying principles. Originally, taxonomy referred only to classifying – or classifications of – organisms. This probably explains that the technique of inheritance in software programming languages is often illustrated with examples of animals or plants.

However, the use of ontological refinement to define classes entails several issues, both on a conceptual and technical level. First, taxonomies are essentially multi-dimensional, i.e., they can be based on multiple criteria. For example, one could refine or categorize a human person based on gender, nationality, or age. Constructing a taxonomy tree based on various criteria could lead to a combinatorial explosion, as nearly all combinations would be possible, e.g., BelgianMaleChild and GermanFemaleAdult. Another option would be to ignore the multi-dimensional nature of the taxonomy, and to use a single primary criterion at a certain level. This approach could limit the anthropomorphic meaning of the classes, as can be illustrated by the distinction between mammals and fish based on the procreation mechanism, e.g., whales and dolphins are no fish although their appearance and behavior is typical for fish. Therefore, it seems often preferable to use references to external classes, such as gender, nationality and age, instead of using inheritance to create subclasses based on taxonomy trees.

Second, the introduction of additional attributes does not always correspond with ontological refinement. Though it is indeed true that a more specific concept exhibits in general more specific attributes, this is not the only mechanism. For instance, a point in 3-dimensional space has an additional attribute with respect to a 2-dimensional point, but it is a generalization instead of a refinement. It is not that rare that attributes of a general concept get default values for more specific concepts and can therefore be omitted. In such cases, programmers are tempted to define the more general concept as a subclass, which is once again detrimental to the anthropomorphic meaning of the classes.

Besides the underlying conceptual issues, the use of inheritance in a traditional object-oriented language creates several technical issues in the context of software development. These issues are mainly related to the introduction of technical coupling between the various classes. Every subclass in the inheritance tree has unrestricted access to the variables and methods of the superclass(es). This coupling is essentially hidden, i.e., the inherited attributes and methods cannot be seen in the class source code, and may be spread out over many classes across many inheritance levels. The fact that modern IDE tools visualize inherited attributes and methods does not really change that. A simple typo can remain invisible during compilation as the compiler may find an actual attribute or method with that name somewhere in the inheritance hierarchy, resulting in the use of the wrong attribute or method. The fact that some languages do not allow multiple inheritance and authors often advise to limit the number of levels in multilevel inheritance, is a clear testimony to the dangers of this technical coupling.

We have often argued that the principle of separation of concerns implies that object-oriented classes cannot contain both data and behavior from a functional point of view. A class needs to either represent data with some auxiliary utility methods, or to represent a behavioral action with some auxiliary utility data attributes. Therefore, we need to distinguish between data and behavior to further elaborate the preferred way to deal with inheritance or ontological refinement.

For behavior functions or methods, we believe that the use of polymorphism is both appropriate and sufficient to provide the required functionality. The mechanism enables the software engineer to provide a variety of implementations that may represent ontological refinement, while realizing at the same time action version transparency for different versions and variants. For instance, an Encrypter interface can encapsulate various specific ways of encrypting a message, while a Publisher interface may be used as a common concept for different ways of message delivery. Every implementation class can be passed in a way that makes the presence of the behavior explicit and allows the programmer to invoke that behavior.

Concerning data attributes or references, we want to stress again that it is often preferable to use external taxonomy entities to avoid the combinatorial explosion of inheritance trees due to the multi-dimensional nature of the taxonomies. A person could for instance have separate taxonomy entities for gender, nationality, and age, or a car could have separate taxonomy entities for the car brand, vehicle type, and propulsion type. Nevertheless, in many cases ontological refinement could be preferable as a number of common attributes may reflect an important common notion or concept that needs to be made explicit. For instance, a home could be an important concept with common attributes that needs to be refined for a house or an apartment. Or a legal person, being a person or legal entity. We argue here once again that the use of polymorphism is both appropriate and sufficient to support such a concept. By providing an interface to represent the common concept, such as a home, the programmer is able to identify, handle, pass, and access the various attributes of this common concept through all ontological refinements.

Information systems still use to a large extent relational databases. To support ontological refinement, we distinguish traditionally three possible options for the database tables as reflected in the drawing below.

  1. One super-table containing the union of all subclass attributes.
  2. Dedicated tables for every subclass, duplicating the common attributes.
  3. Main table with common attributes, and dedicated tables with specific attributes for subclasses.

Option a does not have an explicit representation for the various subclasses, and would lead to lots of empty entries in the various instance rows. Moreover, the introduction of new subclasses – and even new inheritance levels – would lead to an ever expanding structure of the single table.

Option c seems to be the most efficient in terms of database columns or attributes. However, in terms of instances of columns or attributes, it is not. With respect to option b, it requires for every subclass instance an additional type attribute and link key. In addition, it would not be evident to assign an anthropomorphic name to the various dedicated tables. And finally, the software dealing with the persistency data would have to perform some dereferencing logic to retrieve all the attributes, which would not scale well with the introduction of new subclasses or inheritance levels.

Option b seems to have redundant columns for the common attributes, but has less redundancy and more efficiency in terms of instances of database entries. It does not need dereferencing logic, and integrates easily with a polymorphism implementation, where the common concept and attributes are represented as an interface. The fact that the refinement relation is not explicit seems reasonable as the concept is only present in object-oriented classes and not in relational databases.

Based on this reasoning, we have initiated support in Normalized Systems tooling for inheritance or ontological refinement using an implementation that is based on polymorphism for the software classes, and utilizes option b for the organization of the database tables.

2022, looking back at a year of R&D

· 11 min read
Frédéric Hannes
Frédéric Hannes
R&D Engineer
Koen De Cock
Koen De Cock
R&D Engineer

2022 was a fruitful year for R&D at NSX. Adapting to life during the prior two years marked by the COVID-19 pandemic was certainly something the software industry excelled at. Nevertheless, being back in the office and having face-to-face interactions and the open exchange of ideas has also been greatly beneficial to our work.

With the passing of 2022, we want to look back at many of the notable changes coming out of R&D. Several milestones in the development of our modelling and code generation frameworks have opened up many new possibilities for innovation and have accelerated development even further.

Download the presentation

On the Use of Transactions

· 5 min read
Herwig Mannaert
Herwig Mannaert
Founder

The use of transactions in software systems is quite a fundamental and non-trivial issue. Every one knows the obvious example of retrieving money from a cash machine. Either your account is debited and you receive the money, or none of the above happens. In case the machine fails to dispense the money, the transaction requires a rollback of the debiting. However, it is not always that straightforward. Though the use of transactions providing automated rollbacks is often desirable to end-users and system analysts, it may well be unnecessary complicated or even plainly impossible to implement for the software developer.

In order to enable scaling and to avoid deadlocks, transaction processing systems typically deal with large numbers of small things, and strive to come in, do the work, and get out. This means that it is in general not feasible to use technical transactions to provide end-to-end transactional integrity for customer transactions. For instance, guaranteeing the customer transaction of a money transfer is in general not implemented through a technical transaction encompassing the debiting of the source bank account and the crediting of the destination bank account, which may reside in another bank across the globe. It is typically guaranteed by taking the transfer request as fast as possible into a secure database, followed by a number of sequential processing steps or individual transactions. This flow may even require human intervention to make sure that the customer transaction, i.e., the transfer request stored in the secure database, is executed properly.

The use of end-to-end technical transactions would often cause lots of problems in the real world. For instance, defining a technical transaction around the reservation of a flight ticket, a rental car, and a hotel room, could seem desirable to indulge the customer, but would soon lead to the simultaneous locking of all reservation systems around the world. Avoiding such deadlocks is closely related to the NS theorem Separation of States. This theorem states that after every task or action, the result state should be stored in a corresponding and appropriate data entity. This will enable the independent execution, and therefore evolution, of the implementation of the processing task, and the error handling of the task. Besides better evolvability, this strategy improves the tracing and handling of errors, and reduces the chances of having locked resources.

Another often overlooked issue in transaction management is the complexity of the rollback. While a previous write operation in the same database could be compensated by a relatively simple rollback, developers would have to implement dedicated compensating transactions in case the various steps imply changes across different systems. These compensating actions are not always straightforward or even possible. For instance, in case one of the steps has triggered a physical action, like shipping a product or opening a switch or water tap, it is simply impossible to rollback. Unless mopping the floor is part of the compensating transaction.

These issues are also quite relevant when client systems invoke an API (Application Programming Interface) of a target system. Examples of such client systems are business process management systems invoking services from underlying transactional systems, or user interface front-ends developed upon the service APIs provided by back-end systems. The client systems often desires a customer transaction encompassing several service calls on the target system. This requires client and target system to agree on one or multiple scenarios, that define the responsibilities and behavior at both ends. We discuss here the various possibilities.

  • The client system defines the transaction and the target system is expected to honor the transaction. By invoking some start-transaction interface, the client system expects the target system to be able to rollback everything that is being processed until the stop-transaction interface is called. This is a very tough, cumbersome, and nearly impossible requirement to fulfil for the target system.

  • The client system attempts to both manage and honor the transaction. Within the scope of a desired transaction, the client system keeps track of the various changes it incurs in the target system, and will rollback and/or compensate the various changes that have already been performed in case something fails.

  • The target system provides an explicit aggregated service API for certain customer transactions required by the client system. In this case, the target system attempts to manage and honor the transactional integrity for a specific set of customer transactions, thereby providing appropriate rollbacks and/or compensating actions for these specified transactions.

  • The target system provides an asynchronous request API for the aggregated customer transaction required by the client system. The submitted request will be stored immediately in the database, and the target system can start the processing flow performing the various tasks, compensating already performed tasks if required, thereby even supporting manual interventions. The client system can choose to perform other tasks while checking regularly for an answer, or opt to wait or block until the aggregated service is fully executed. The latter option would emulate a synchronous service call at the client side.

On Hierarchical Workflows

· 2 min read
Herwig Mannaert
Herwig Mannaert
Founder

Decomposing business process models into state machines, it is quite common to encounter so-called hierarchical workflows, i.e. the creation in a workflow of an instance of another data element, which triggers a secondary workflow. We distinguish two possible scenarios.

  • Fire and forget: a workflow task triggers a secondary flow, which can be processed in an independent way.

  • A workflow task triggers a secondary flow, but the task in the primary flow can only be considered complete when processing is completed in the secondary flow.

In the second scenario, one should be cautious about possible dependencies and the corresponding coupling. More specifically, coupling may surface in two ways.

  • In case the primary workflow and/or task is polling the secondary flow that it has triggered, this could lead to lots of threads and coupling in time between software modules.

  • In case the secondary workflow has to report back to the primary flow upon completion, this could lead to a bidirectional dependency between the two workflows. This could result in a technical issue in case the two workflows belong to a different software component.

In order to avoid coupling and even possible locking in time, NS theory suggests that the secondary workflow needs to report back upon completion. However, to avoid bidirectional coupling between the code of both workflows, this should be done using a loosely coupled mechanism. An example of such a loosely coupled mechanism would be a service interface on the primary workflow. This would allow the primary flow to pass the URL of that service interface as a callback function to the secondary flow, avoiding tight coupling in the source base.

Based on the confirmation or completion message of the secondary workflow, the primary flow will set the corresponding status on the target data element of the primary flow. In this way, the secondary workflow does not need any knowledge of the possible states of the primary flow. In case multiple subflows need to be processed and completed before the primary flow is able to continue, it is again (a task in) the primary flow that needs to have the appropriate knowledge to combine the information provided by the various subflows.