Friday, July 14, 2023

Software Documentation: Less is More, Architecture Decision Records and arc42

"Less is more" is true for many things and especially true for Software Documentation. If you need to document your source code, I really believe that this will not help, because most of the time the problem exists on a higher level: poor code quality, no or very few tests, non modular design and violation of the SOLID principles (https://en.wikipedia.org/wiki/SOLID). As long as you don't generate your documentation from the current source code automatically, it is very difficult to keep the documentation in sync. Because the source code contains many details and can be subject to frequent changes, this can lead to deprecating your documentation faster than you might think. Furthermore, Quite often the documentation is not kept in a single location but distributed over many systems and written by many different people. Therefore, you don't know all the occurrences of the documentation that you have to update or adjust when you change certain parts of your source code.

The big question is, what do I document and how do I document it? As written before, you should not document details - the specification of your software - because details are subject to change (e.g. when you refactor your code, which should be done on a regular basis). Good automated test cases will serve as an executable specification (https://www.martinfowler.com/bliki/SpecificationByExample.html): just look at your tests, if you want to know more details. So, what should we document? I believe that the most valueable and important documentation is about the things that are the least obvious. For example: consider that you have a Java service that uses a PostgreSQL database. What is the benefit, when I document the fact that the service uses a PostgreSQL database? I already can see this when I look at the source code. So in this case, the documentation does not give me any added value - it's just one more thing that I've to keep updating when things change. What should be documented is the decision WHY we use a SQL database and for example not MongoDB. This is a problem that can be solved by using Architecture Decision Records (https://martinfowler.com/articles/scaling-architecture-conversationally.html). There are different templates for ADRs and I like the one from Michael Nygard, because its simple and straight forward (https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/templates/decision-record-template-by-michael-nygard/index.md). Now, you have to write the ADR which probably is the most difficult part. Focus on "less is more" and try to be as clear and precise as possible. Because I believe that everything that is closely connected to the source code, should be part of the source code repository, the ADR should be versioned in the repository together the source code and have its own section in the README. This makes refactoring more easy, because you can utilize the full text search of your IDE and it gives you a better chance to keep documentation and source code in sync.

But most likely, ADRs which are part of your repository will not solve all the documentation problems that you are facing. You might also need some more high level documentation and your company maybe want you to write such a documentation in something like Confluence. It could also be the case that you have to use a different language for the higher level documentation than for the ADRs (e.g. because ADRs can be a team decision, but the company "policy" dictates that all teams have to write high level documentation in Confluence and in German for example). If you need to document more and if you want to write a more sophisticated Software Architecture Documentation (SWAD), you can use the arc42 template (https://arc42.org/). The arc42 template defines 12 "chapters" that you can use for your Software Architecture Documentation. These chapters cover all the important categories which are usually important. in a Software Architecture Documentation. Again, the important thing here: less is more or "travel light". This means that you should only put the chapters from the arc42 template into your documentation that you really need. If for example you don't have anything to say about 7. Deployment View than don't try to write something about it.

Friday, July 7, 2023

How to provide Angular Environment Configuration after Build Time

When you want to deploy a containerized Spring Boot application to Kubernetes and provide environment specific configuration during deployment time (e.g. with a Build Pipeline), this is very easy: you just have to set operating system environment variables in the context where your container process runs and these environment variables have to match your Spring Boot configuration properties in order to provide the configuration you want to use (https://docs.spring.io/spring-boot/docs/3.1.1/reference/htmlsingle/#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables).

But this approach does not work with Web Applications which for example are build with Angular and run in a Browser. For example, you can use the official Nginx Docker Image to deploy an Angular app to Kubernetes and you could set operating system environment variables which then can be accessed by the Nginx process in the container . So far, no problem. But how do you want to access such environment variables in a Browser, which runs on a completely different machine than Nginx? The only way to communicate from the Browser with Nginx is via HTTP requests. Furthermore, the default Angular environment.ts configuration can only be used to provide configuration at build time, which means that this configuration will be part of the Docker Image.

Therefore, you have to "inject" a file into your Nginx container, which holds the environment specific configuration. Such a file can then be loaded from the Browser via a HTTP GET request and processed by your Angular app. It makes much sense, that such a configuration file contains JSON data, because this is easy to process with Angular. One important aspect when you are building "cloud native" apps is that you want to build your Docker Image only once and then use it for all environments (e.g. local, dev, prod). To make this work, you have to separate configuration from code, which should automatically be the case when you have a clean and modular design. To separate configuration from code is also one factor in the Twelve-Factor App (https://12factor.net/config). One generic way to "inject" a file into a container at Deployment time is by using Kubernetes Volume Mounts in combination with a Kubernetes ConfigMap (if you're using Kubernetes; see https://kubernetes.io/docs/concepts/storage/volumes/#configmap).

The recommended way to load the configuration into your Angular app is by providing an APP_INITIALIZER which will load the configuration when your Angular App bootstraps (https://angular.io/api/core/APP_INITIALIZER). Because of the asynchronous nature of Angular and the underlying Observer Pattern, the configuration is only accessible through an Observable (https://angular.io/guide/observables). This means that when you make a GET request with the Angular HttpClient, you will get an Observable that holds the configuration as a response. As a consequence, you have to transform this Observable in all areas of your code where you need to access your configuration. Let's say we have a RemoteConfigService which will load the configuration when the APP_INITIALIZER is invoked. The RemoteConfigService can now provide access to the configuration by declaring a getter named getAppConfig with the following signature:

getAppConfig: Observable<AppConfig>

When you want to access a certain property of your JSON configuration, you can use the pipe/switchMap pattern (from rxjs) to transform the Observable. The following method demonstrates how to do that:

getPropertyFromConfig(): Observable<string> {
    return this.remoteConfigService.getAppConfig().pipe(
        switchMap(config => of(config.myPropertyName)));
}

The return value is another Observable which holds the property value. You can directly access the Observable inside an Angular HTML template by using the "async pipe" (https://angular.io/api/common/AsyncPipe).