principles_of_developing_robust_applications

Wednesday, October 29, 2025, 2:57:21 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Functional core, imperative shell

Imperative code with state and side effects is hard to write, understand, test and debug. The languages we're working with are mostly imperative though, but they have functional features.

Try to write the core business logic in a functional way, without state and side effects. Then use an imperative shell to handle all side effects, while using the functional core.

Exceptions and how to handle them

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

Constant resource usage

Try to keep resource usage constant or at least try to limit its growth.

Log rotation

Constantly growing logs will fill up your disk sooner or later. Log rotation will keep this disk usage constant (depending on how rotation is done).

Usage statistics

Another example would be website visitor statistics. Linear growth could be limited if, after a certain condition is met, the raw log data is compiled into a fixed size summary and afterwards deleted. This still means somewhat linear growth, but a new summary every month is much more managable.

Memory

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.


    // 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:

    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

References

source


Wednesday, October 29, 2025, 2:17:41 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Functional core, imperative shell

Imperative code with state and side effects is hard to write, understand, test and debug. The languages we're working with are mostly imperative though, but they have functional features.

Try to write the core business logic in a functional way, without state and side effects. Then use an imperative shell to handle all side effects, while using the functional core.

Exceptions and how to handle them

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

Constant resource usage

Try to keep resource usage constant or at least try to limit its growth.

Log rotation

Constantly growing logs will fill up your disk sooner or later. Log rotation will keep this disk usage constant (depending on how rotation is done).

Usage statistics

Another example would be website visitor statistics. Linear growth could be limited if, after a certain condition is met, the raw log data is compiled into a fixed size summary and afterwards deleted. This still means somewhat linear growth, but a new summary every month is much more managable.

Memory

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.


    // 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:

    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

References

source


Tuesday, October 28, 2025, 4:09:06 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Functional core, imperative shell

Imperative code with state and side effects is hard to write, understand, test and debug. The languages we're working with are mostly imperative though, but they have functional features.

Try to write the core business logic in a functional way, without state and side effects. Then use an imperative shell to handle all side effects, while using the functional core.

Exceptions and how to handle them

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

Constant resource usage

Try to keep resource usage constant or at least try to limit its growth.

Log rotation

Constantly growing logs will fill up your disk sooner or later. Log rotation will keep this disk usage constant (depending on how rotation is done).

Usage statistics

Another example would be website visitor statistics. Linear growth could be limited if, after a certain condition is met, the raw log data is compiled into a fixed size summary and afterwards deleted. This still means somewhat linear growth, but a new summary every month is much more managable.

Memory

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.


    // 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:

    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

source


Tuesday, October 28, 2025, 3:57:38 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Functional core, imperative shell

Exceptions and how to handle them

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

Constant resource usage

Try to keep resource usage constant or at least try to limit its growth.

Log rotation

Constantly growing logs will fill up your disk sooner or later. Log rotation will keep this disk usage constant (depending on how rotation is done).

Usage statistics

Another example would be website visitor statistics. Linear growth could be limited if, after a certain condition is met, the raw log data is compiled into a fixed size summary and afterwards deleted. This still means somewhat linear growth, but a new summary every month is much more managable.

Memory

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.


    // 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:

    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

source


Thursday, October 23, 2025, 1:21:36 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

Constant resource usage

Try to keep resource usage constant or at least try to limit its growth.

Log rotation

Constantly growing logs will fill up your disk sooner or later. Log rotation will keep this disk usage constant (depending on how rotation is done).

Usage statistics

Another example would be website visitor statistics. Linear growth could be limited if, after a certain condition is met, the raw log data is compiled into a fixed size summary and afterwards deleted. This still means somewhat linear growth, but a new summary every month is much more managable.

Memory

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.


    // 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:

    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

source


Thursday, October 23, 2025, 12:49:13 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

Don't log and rethrow - this just leads to double logging. Logging is a form of handling, so do either the one or the other.

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

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.


    // 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:

    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

source


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

Principles of developing robust applications

Kotlin edition

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.

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:


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

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.


    // 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:

    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

source


Sunday, October 19, 2025, 10:02:54 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

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:


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

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.


    // 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:

    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.

source


Sunday, October 19, 2025, 9:52:22 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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.

{
    "id: 1,
    "app_name" : "hello world",
    "file_backend" : "data.json",
    "database_backend" : null
}

// or 

{
    "id: 1,
    "app_name" : "hello world",
    "file_backend" : null,
    "database_backend" : {
        "ip" : "127.0.0.1",
        "username" : "dbuser",
        "password" : "pwd"
    }
}

The class this gets deserialized into looks like this:

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.

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

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.


    // 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:

    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.

source


Sunday, October 19, 2025, 9:49:56 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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).

{
    "id: 1,
    "app_name" : "hello world",
    "file_backend" : "data.json",
    "database_backend" : null
}

// or 

{
    "id: 1,
    "app_name" : "hello world",
    "file_backend" : null,
    "database_backend" : {
        "ip" : "127.0.0.1",
        "username" : "dbuser",
        "password" : "pwd"
    }
}

The class this gets deserialized into looks like this:

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,
)

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

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.


    // 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:

    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.

source


Sunday, October 19, 2025, 9:37:06 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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).

For example: in a web request we get a response for an item, but the item is

{
    "status" : "success",
    "data" : "foobar",
    "error" : null
}

// or 

{
    "status" : "error",
    "data" : null,
    "error" : {
       "code" : 17,
       "message" : "The item is not yet ready."
    }
}

Regularly, we'd have a Response class that

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

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.


    // 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:

    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.

source


Sunday, October 19, 2025, 9:24:16 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

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. This enables enumeration of all cases (see next section).

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

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.


    // 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:

    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.

source


Sunday, October 19, 2025, 9:22:24 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

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

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.


    // 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:

    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.

source


Friday, October 17, 2025, 2:23:32 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

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

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.


    // 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:

    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.

source


Friday, October 17, 2025, 1:06:57 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

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

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.


    // 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:

    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.

source


Friday, October 17, 2025, 12:57:44 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

Specific types with value classes

Exceptions and how to handle them

Logging

Log as little as possible and as much as necessary.

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."

Different log levels:

Memory usage

source


Friday, October 17, 2025, 12:11:18 PM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

Specific types with value classes

Exceptions and how to handle them

Logging

Memory usage

source


Friday, October 17, 2025, 9:39:23 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

Exceptions and how to handle them

Logging

Memory usage

source


Friday, October 17, 2025, 9:38:35 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Union types

Validate at the boundary

Null handling

Enumerating all cases

Exceptions and how to handle them

Logging

Memory usage

source


Friday, October 17, 2025, 9:30:25 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Validate at the boundary

Exceptions and how to handle them

Logging

Memory usage

source


Friday, October 17, 2025, 9:22:40 AM Coordinated Universal Time by stefs

Principles of developing robust applications

Kotlin edition

Making illegal state unrepresentable

Exceptions and how to handle them

Logging

Memory usage

source


view