principles_of_developing_robust_applications

# Principles of developing robust applications

Kotlin edition

[TOC levels=2-6]

## Making illegal state unrepresentable

### Sealed types (union types)

Sealed types in kotlin are subtypes of a certain class or interface with all subtypes being known at compile time - think of them as enums which can carry individual data per instance. This enables enumeration of all cases (see next section), which can be used to enforce handling of all variants at compile time. But most importantly, it makes it possible to handle distinct class cases without having implicit field combinations (that are checked at runtime).

In our example we're managing applications, and those applications can have different types of storage backend - let's say password-protected databases and local files (which are not password protected). It's _either_ a file backend _or_ a database backend, but never both and having none is also not allowed.

```kotlin
data class ApplicationResponse(
    val id : Long,
    val appName : String,
    val fileBackend : String?,
    val databaseBackend : DatabaseBackend?,
)

data class DatabaseBackend(
    val ip : IpAddress,
    val username : String,
    val password: String,
)
```

The problem here is that, at the type system, it's possible to create instances of the class `ApplicationResponse` that are illegal according to our spec. You could have either both file and database backends defined - or none.

We could ensure that by enforcing this with a validator:

```kotlin

data class ApplicationResponse(...) {

    init {
        require(fileBackend != null XOR databaseBackend != null)
    }
}

``` 

This is still not optimal, because this can lead to RuntimeExceptions.


### Enumerating all cases

### Validate at the boundary

### Null handling

### Specific types with value classes

## Exceptions and how to handle them

## Logging

Log as much as necessary but as little as possible. Messages should have enough context to be valuable. This means if you read a log message it should be useful. Who did what, when and where: "Document updated" is less useful than: "User A updated document D, changing the title from Y to Z."

### Log Levels

* Verbose: Use this to print raw input / output from external sources.
* Debug: Provide context that is useful for debugging, i.e. making internal state visible, log the path taken in important conditionals
* Info: Use this to log (user) and system actions that were done purposefully, i.e. triggered by an external event.
* Warning: Use this for errors that can be recovered from.
* Error: Use this for unrecoverable errors.

### Log rotation

Don't forget to enable log rotation, otherwise your application will go down unexpectedly because of a full disk.

## Memory usage

If possible, try to keep the memory usage constant when working with external resources. Use streams instead of loading them into memory if the size is undefined.

For example, this takes as much memory as the file is big. If the file's bigger than the JVMs heap allowance, it dies with an OOM.

```kotlin

    // read file content into memory
    val content = Files.readAllBytes(Paths.get("source.jpg"))

    // write file content to disk
    Files.write(Paths.get("target.jpg"), content)
```

On the other hand, the following code:
```kotlin
    FileInputStream("source.jpg").use { inputStream ->
        FileOutputStream("target.jpg").use { outputStream ->
            inputStream.copyTo(outputStream, 4096)
        }
    }
```
takes roughly 4096 bytes at once, no matter how big the file is.

Often, this can't be avoided when you need the whole object in-memory to process it. Even in this case, though, you can often avoid having multiple representations of the same object (in different states) in memory. This means loading the whole document from a stream instead of a byte array.

## Other

* Use sensible defaults: sensible defaults should concentrate on _safety_

edited by: stefs at Thursday, October 23, 2025, 11:05:33 AM Coordinated Universal Time


view