Tactical Design by Example - Using Kotlin and Spring Boot (Part 9) - Behaviour, not data

Let's take a look at the evolution of an application: Today, most development is done in iterations, so applications start out with a minimal feature set, and are growing steadily over time. It is not uncommon to end up with very complicated code after only a few iterations, that is hard to extend and maintain. Often, a rewrite or abandonment are the only options left.

This is not a problem per se. But I'm arguing here that a data-centric perspective might be a cause of this phenomenon, and a behaviour-centric perspective might help reducing complexity and allow lengthening the life-cycle of an application. So it might be a good idea if you want to create something of value, something to rely on in the long run.

Let's sketch a very broad picture in this blog post of what I mean by a data-centric perspective.

Data-centric perspective

Most applications start out small: Usually, there is a client-side and a server-side component, where the job of the server-side component is to do persistence or publication, and expose itself to the client-side through a web interface. Let's focus on the server side for now.

For persistence, a relational database is still a popular choice. For a web interface, a REST API is the default option. As far as programming languages are concerned, object-oriented languages like Java are still an industry default. I'm arguing that this tool set will most likely lead to the application being modelled as a CRUD application.

CRUD

In a CRUD application, there are only four kinds of behaviours: create, read, update, delete. These are also the basic HTTP verbs, and also the basic relational database operations (insert, select, update, delete). Object-oriented languages are traditionally (mis?-)using objects as data containers, exposing the data via getters and setters.

So the choice of the tools will have an immediate effect on the solution of the problem: When you have a hammer, everything looks like a nail.

Applications keep growing

The limitations of CRUD are of course also charming: The concept is widely understood and usually progress is made really fast. If the scope of the application in question is strictly limited, it might be a good choice. But usually, the limitations are exceeded rather sooner than later, and to make it worse, developers are hardly ever aware of that.

Symptoms of a data-centric perspective

JPA is overwhelming

In a Spring application that is designed for CRUD, JPA is a popular choice for accessing the database. It is an overwhelmingly powerful tool, yet it's deceptively easy to use - a dangerous combination. It works really well for standard use-cases, but it forces your application to obtain a database-centric view, because the JPA entities themselves are modelled after the table layout.

JPA entities are also mutable, because changes are only flushed to the database when the object is actually changed. Because immutability is discouraged, side-effects are not uncommon and more complexity is added.

Almighty services

Usually in this setup, the application services are clustered by entity. So if you have an "order" entity, you usually have an "order" service. This works quite well if you only have one entity: but as soon as two entities are depending on one another, it soon becomes arbitrary in which class an application service method is put, and you might end up with circular dependencies.

These service classes usually grow to an unmaintainable size. There is no purpose, no definition of what their job is. Often, they absorb all application logic like a black hole, because JPA entities are often anemic, and do not contain logic. If you are using getters and setters extensively, your domain model is likely to be anemic.

All this makes testing hard: If a class is huge, it also has a huge test class, where you easily get lost when trying to find the right test case. This leads to missing test cases, and also overlapping tests. Also, mocking becomes a nightmare, because a service all of a sudden has loads of dependencies.

Refactorings often only scratch the surface, because the keep the underlying system in place, and only move a bit of code from left to right. Improvement within this frame of mind is hardly possible.

Behaviour-centric perspective

Apparently adding new features is hard when code has to be altered. Adding and removing code is easy, but changes are hard. If your code is structured in a data-centric view, consequences of changes are often global, because the data is used everywhere.

We would like to have a code base where new features translate to new code, not altered code. This is what I mean by behaviour-centric: any new feature is a new behaviour, which can be added as new code rather than changing existing code. We need to structure our application in a way that allows this style of development.

I will elaborate on behaviour-centric development in the next weeks, so stay tuned!

Move on to part ten or go back to part eight.