About this talk
This talk explores how to enhance Spring Boot applications using Kotlin and its powerful Domain Specific Language (DSL) features. The speaker, Urs Peter, highlights the importance of test code in software development, asserting that effective testing is as crucial as production code. He discusses various tools and libraries such as Kotest for matchers, assertJ for fluent assertions, and PowerAssert for detailed failure messages. He demonstrates the advantages of Kotlin's concise syntax, null safety, and type-safe builders, all of which contribute to cleaner and more maintainable testing code. The session also covers advanced topics such as coroutines for handling asynchronous programming and Kotlin notebooks for exploratory testing. By the end of the talk, attendees will appreciate the benefits of adopting Kotlin in their Spring projects, leading to improved developer experience and code quality.
Full transcript
[music] >> Welcome to my talk about how to supercharge your Spring Boot application with Kotlin DSL power. So, how is Spring Spring I/O so far? Good? Do you get enough Kotlin? Oh, that's what I wanted to hear. Great. Okay, so before we start, let's make this a nice memory in beautiful Barcelona. So, just express how much Kotlin you would have to see on this conference. Yeah, is
this enough? Yeah, yeah, that's better. That's >> [laughter] >> Okay, so this talk we try to change that a slight bit. Okay, little introduction about me. I'm Urs Peter, I'm a senior software engineer, mostly trying to be with my hands in the dirt. I'm also a passionate trainer helping you Kotlin developers to actually reach the next level, no matter whether you are a beginner, intermediate, or advanced
Kotlin developer. I'm also a JetBrains certified trainer. You probably have seen me talk in conferences, and I'm also a co-organizer Kotlin Dev Day in the Netherlands, Amsterdam. Has anybody joined Kotlin Dev Day? Okay, so that's something you probably going to put in your agenda. It's a very nice conference dedicated fully to Kotlin, mostly about back-end development. When you look at my career, I started with Java, moved
over to Scala quite some years ago, and for the previous 6-7 years foremost I write my logic in Kotlin. Okay, so enough about me. I'm actually quite curious about you. So, who of you is using Java as a primary language? That's quite a few. Okay, cool. And the rest I assume it will be Kotlin. Is there anybody who's using Kotlin foremost for testing? A few ones, few
ones. Okay, okay. Not really majority, but well, we'll figure that out. All right, so when we look at the proportion of production code versus test code, then we see that for a traditional average healthy project, we see about a one to one proportion. So, as much test code as you have production code. When we look at a mature back-end that has aged over time, then it's even
more. It's one and a half time as much test And when you build critical applications, generally you have about twice as much test code as you have production code. The applications I personally work on has indeed about two to one, three times the amount of test code than it has production code. And I believe with the advent of autonomous agent development, this number is simply going to
increase because you want to guardrails those LLMs that hallucinations stay within bounds. So, it really is of paramount importance that you ensure that your test code is an enabler rather than a liability, right? When we look at the capabilities Java offers us in that field, we could say it does a decent job. Right? So, if it was a rocket, you probably would reach low Earth orbit. However,
today we want to go much further. We want to reach the end of the universe, at least when it comes to our testing capabilities. And you'll figure out where Kotlin can get us there. Well, for this I have created a test has a domain I assume most of you are familiar with. It's a conference domain with talks and speakers and ratings and tags on talks and so
on. So, this is the domain we're going to look at. What I assume many of you know, but maybe some don't, is that Java and Kotlin can perfectly coexist next to each other as a one happy family. So, I can call Kotlin code from Java, I can call Java code from Kotlin. It just works. So, there's no reason to not introduce Kotlin in your Java project. let's
speed things up because this rocket is going to be launched in a countdown of about 10, and then we need to have it supercharged with Kotlin. So, let's hurry up with our count going for nine. And the first thing we want to look at are Kotlin matcher DSLs. What could they be as capabilities to our test suite? First, we're going to look at a very basic test
in Java. split this up. There we go. So, what you see here, as that is a very basic example with a tag service we're going to test, and then you have the basic assert equals helpers to ensure that our test is asserting the right stuff. If you had to write the same thing in Kotlin, it would most probably look a bit different. So, first of all, we
could use the backticks to define our test definition, which gives us a much more readable definition of our test. Especially when you have these lengthy camel case definitions, it's kind of hard to read what's actually going to be tested. And by having a sentence, it just helps. Okay, when it comes to the actual matchers, we most probably wouldn't use assert equals in Kotlin. You would use something
like a matcher library. The one I'm going to use here is Kotest. So, who of you has heard of used Kotest? Quite a few of you. Okay, cool. So, what is the advantage of having something like Kotest over assert equals? First of all, it's a DSL, meaning that it reads very natural, like human language, right? Create a tag should have size one. It's also nice that these
assertions or these these matchers, they show up in code completion. And they show up in code completion for types that are relevant for the for what you would try to assert. So, here created tags is a collection, and now I only get all the matchers for collections. Moreover, they are have there is a great variety of matchers to very concisely precisely assert what you would like to
assert. Whereas, of course, when you have assert equals, you have many assert equals which maybe are not relevant for the type you're trying to assert. But there is more. So, when we look at failure definition we get when we assert equals, you simply would get this particular case, two should be one. The outcome would be two is not one, which is not tremendous helpful when it comes
to um showing you the root cause of what is going wrong. Whereas, the matchers here, they're slightly more convenient and more expressive in the result they deliver. So, there we go. Um Now, it was successful. That's probably not what you want to see. We want to see it fail, which should be done in a second. And you see now that we do not get two is not
one, we get collection should have size two, but has size one, and those are the values, and so on and so on. So, gives you just more information on what went wrong for you then to correct it. Moreover, what I also can make use of in Kotlin is just standard Kotlin features, which already help you help us a great deal of defining our test as concise as
possible. So, what we are using here is apply, that's a method which is available in the standard library on all types, so to say. What apply does is it brings us this particular instance here, so the first tag, into this scope. Right? So, when you have you the this keyword here, it actually refers to this particular instance. Whereas, in Java, I would have to define a var
and then use this var all over and over again. Then also here name should be should be is probably the most similar to one to assert equals, but because name is a string, you also get a huge variety of matchers on spring, where for instance could do a regex match and so on and so on. So, collection matchers, string matchers, and there's really a great amount of
matchers out there to assert exactly what you want to assert. Okay, so far, so good. Then, let's move on to a slightly more complex example, which uses assertJ. So, I assume most of you are using assertJ. Who is using assertJ? Okay, I guess when you come from the Java ecosystem, it's very common to use Well, the interesting thing is with Kotlin, you actually do not need Why
is that? Let's figure that out. So, what we are doing here is giving the equivalent of the first block we have here on the left. So, assert that. And again, the combination of standard Kotlin language features like apply, but also standard library or standard collection method like for each, map, and so on, actually makes it obsolete for us to not have to to of using a sophisticated
DSL like in assertJ that allows us first of all to assert certain values, but also to extract for instance certain attributes. The extraction can simply be done by calling standard collection methods in Kotlin. So, you don't need to learn this type of DSL. Okay. This also is reflected when we look at exceptions are sorted. So, you see here should throw, we simply define the type and from
there on you would get exception as return type, say dot message should contain in order. And the cool thing in Kotlin is that also a string is a collection. It's a collection of characters. And as such, I can use all the collection matchers on string, too. And there is another slight benefit. You might know that within the assertJ, all the assertions are accumulated. It doesn't fail fast,
right? With the first assertion that doesn't fail. So, it accumulates all the assertions and then gives a result of all the assertions. Which is limited to a single assert that call chain, right? Using Kotest, you also have this nice wrapper function called assert softly. And everything then within this particular block would accumulate all the assertions. So, it would only fail when all the assertions have been executed
and give you a full result of what went wrong. Which gives, of course, much more granularity than with assert that. So far, so good. the benefits of Kotlin DSL matchers is, as we already seen, they are very expressive, they're human readable, you have better code completion, you have messages. Definitely worth to try it out if you don't do it yet. And it just, as you have seen,
you can combine it with JUnit. You don't have to kind of use a different testing framework for that. All right, let's move on with our countdown to number eight, which is PowerAssert. Who of you has heard of PowerAssert? Wow, cool. Then you're going to learn something new today. it's kind of interesting what PowerAssert is because at first glance it looks very, very primitive, basic. So, I have
another test here with just, again, very traditional assert equals. And now we're going to do the same assertions with PowerAssert rather than, well, assert equals or the Amatch library like Kotest. And this is how it looks like. assert and then just some arbitrary logical conditions. And you might think, okay, what is this? I mean, it's probably going to deliver a true or false, test failed because of
false, the most useless definition you can have for failing test. But that's not what happened because here the power of assert is revealed. Take a look at that. So, when you run this particular test, you will see that we do not get failed because of false, but we get a quite specific definition of what went wrong with our condition. So, you see here talks, those were the
values of talks, hat size one, which was not size two, and so on. This actually accounts is accountable for every particular assertions we do with just basic logical operators. So, it's kind of a neat way, more or less, with assert you can assert everything and at the same time still get very rich information of what exactly went wrong. The way this works is that a compiler plugin
is triggered when you compile stuff, which then compiles basically or recompiles all these logical conditions in a way that this information can be retrieved. So, it's kind of a sophisticated thing, but, yeah, might also be a valuable to consider. Okay, let's assume that you love assertJ, right? And you have done it for a long time, you think, why should I change? I know how this works. Also
there, Kotlin can give you some benefits. So, for instance, what you see here on the left in Java, if you want to assert certain properties, then you normally would use strings for that. And I can see it probably um in your eyes that this is a kind of dangerous thing. If I start to refactor code, then if my ID doesn't do the job very well, most probably
these attributes wouldn't be refactored if I renamed bio to biography, right? And then I get another unnecessary error in my code. Kotlin's reflection is a bit more powerful than the Java one. So, what we see here on the left is that when you reflect on a field, you do not only get the function to call that the getter, so to say, you also get additional attributes like
name. So, here the name of these attributes is type is provided in a type-safe manner. So, when I refactor my bio to biography, then this would definitely be refactored, too. So, it just gives you more safety. Not only for tests, by the way, but also in all the other code you write, that's a very safe way of using attributes in the in the string form so that
it's um guaranteed to be type-safe. All right, let's move on. I assume that one of the maybe biggest challenges we have with testing is dealing with test data, right? Especially when you have a bit a larger domain, test data is a challenging thing to kind of tackle in a effective way. let's take a look at an example here. What Java can deliver to us and where we
kind of get stuck. So, what we're trying to test here is basically a talk. And to create a talk, we first need a speaker, right? We need to persist the speaker and then we need to create the talk passing the speaker we have just persisted. And can you imagine if I have [snorts] to test a variety of talks, copying this particular snippet over and over again would
lead to a lot of duplication in my test code base. Which is kind of a bad thing, especially when I add new attributes, I would have to adjust a lot of tests to make things in sync again. So, how could we solve this with pure Java, right? Let's figure that out. And I'm quite sure you know already the answer. We could use something like object model builders,
right? Object model pattern, you probably heard of it. It's just a kind of a utility that generates or creates some test data. And in Java, this would be builders. So, a builder would look like this, just traditional builder fields with some default values so that I had already a good set to start with. And then I would have to just make some adjustments for the ones that
are relevant for my particular test. Um that's kind of okay-ish, right? But you still need to maintain a large builder base and all these attributes it's okay, but not perfect. And there's actually another problem with builders, which is not only for these kind of builders an issue, but with builders in general in Java. It's kind of hard to tell a builder which attributes are mandatory and which
are not, right? That's really a thing with builders. For instance, in this particular case, I really need to assign a speaker to a talk. You wouldn't have a great talk if you wouldn't if you wouldn't be a speaker. And there is no type-safe way to ensure that speaker is provided when I create such a talk. So, I can just comment with speaker out and my code still
compiles. So, let's see if Kotlin can do a better job here. And the interesting thing is that with very, very basic Kotlin features, you can actually solve this problem in a more, considerably more elegant way. So, first of all, in Kotlin you have so-called top-level methods. You also have them now in Java, at least for the main method, not for the rest. Kotlin has them for everything.
So, every method you can define top-level, not bound to a class or an object. That's for one. And second, Kotlin provides default arguments. Great feature. Um when you use the @JvmOverloads annotation, the compiler would even generate all the overloaded variations considering the the arguments you have provided. And these then could be used in Java, too. Right? So, you can actually get a lot of rid of a
lot of boilerplate even if you would use this particular method in Java. Then you can combine default arguments with named arguments. And as such, just change the argument I would like to change without providing the whole signature as you would have to do in Java. As you might have heard of, Kotlin is a safe language. it's really considerably safer than Java in many ways. And the most
well-known safety feature in Kotlin is null safety. It's definitely not the only one, there are many, many more if you look closer, but null safety is probably the most important. What does this mean? Well, in Kotlin, you cannot just assign null to anything. You only can assign null to a nullable type. And nullable types in Kotlin are denoted with the question mark. So, only if I have
a string question mark, and I can assign null to it. Otherwise, if I don't have the question mark, null cannot be assigned. So, assigning null to name or email is not possible. You get a compilation error. So, it's all captured at compile time and not at run time. these features together can actually do a decent job to solve the problem we just have seen with our builder
approach. Let's see in action. So, what I've defined here are builder methods, object model methods so to say, with some default arguments, including nullability if if applicable, with the JVM overloads uh annotation. And now, in my test, I simply can use those. So, by simply declaring a single method, I'm done. No huge builder per file and so on. A method, even multiple methods in a single file.
And then, when I want to change certain attributes, I use again my named arguments, and there we go. Problem solved. Moreover, we now also have a solution for mandatory attributes. So, for instance, here, the speaker needs to be passed because, well, when we look at the builder method, we see that the speaker is not nullable and it has no default value, so you need to provide it.
And if we would provide null, guess what? We get a compilation error. That's exactly what we want. We want the compiler to work for us and not figure out stuff at runtime. And also, not providing a speaker wouldn't work, would also uh yield a compilation All right. So far so good. In many cases, you already have a piece of test data, but you just want to have
a variation of it instead of recreating it all over and over again, maybe you just want to do a slight adjustment. So, let's see um how far Java can get us there. So, here we have a primary speaker request, and then we would create a co-speaker from the same company, and the search actually rely on the primary speaker as a reference. And only approach we have in
Java is basically um yeah, provide all that attributes, and then copy the ones which of our object we use as reference, which, especially in this particular case, is also a bit dangerous because um mixing up company and biography is easily done because they're both strings, right? And there's also no uh no uh label anymore telling me that I'm assigning something to company. So, it's not a nightmare,
but it's definitely a section where you can very easily get so-called stupid errors. Is there a solution to this problem? Yes, we could actually extend our builder with a from method, right? So, you would take a a record and then pass in this record, which I would say works fine. However, um the disadvantage is that, again, we have to um build in this in all our builders.
how could Kotlin solve this particular problem? Well, Kotlin has data classes. Who knew I've heard of data classes? I guess most of the ones who have worked with Kotlin. Yeah, cool. Well, data classes and records are kind of um compared to each other as the similar thing, but that's not true. Actually, data classes are a superset of what records offer. Let me explain. So, first of all,
when I annotate a JVM uh a data class with JVM the Kotlin compiler would generate a record, so Java can use it as a native record. Java wouldn't even know that the underlying thing is not a record. For Java, it's just another record. Then, of course, we have nullability and um and non-nullability, which is also translated in a way that when the thing is compiled, the compiler
would automatically um compile at nullable and not null annotations into the record. Right? So, our record get also more feature-rich than if you would do it yourself. Then, of course, we have again our default values, which is nice, right? You don't have to do overload the constructor in our record. And we even could deduce some values from another argument. That's just the beginning. Now, the real cool
thing comes here, which is copy. Copy really is what you what you miss in records. And we'll see how how this works in action. But there is even another feature which really makes a big difference, which is that you can define multiple data classes in a single file, and they're all public, which gives you the possibility to actually aggregate a subdomain that is closely related to each
other in a single file. So, when you just scan over it, you see, "Oh, yeah, these are all the classes that are related to one another." Instead of having per record a separate file, which is very hard to get an overview of how your domain is looking like. Okay. So, let's see what would happen if we used data classes instead of records, but still keep Java as
a primary driver. So, in this particular case, we already were using data class. So, here is a record. Java thinks it's a record, but it's not. It's a data class. Yay. And you see, we can have multiple of these in a single file, and as such, gain a very quick overview what this domain is all about, which is also applicable, of course, for AI. It just just
has to read a single file rather than go through a lot of variety of other files. Okay. So far so good. And now comes actually the power of copy. Copy is so awesome. Copy allows you to create, guess what? A copy of another object. So, we have here our primary speaker, we say copy, and then again, combined with named arguments, we are capable to only adjust the
things you want to adjust, and then get back another immutable instance that is ready for operation. Not having to pass uh all the prior states. Also, the chance that you assign wrong values to name or email is much lower because you have the label really in your face, and so on and so on. So, a variety of benefits. So, if you have a record-heavy Java application, that
might definitely be a feature you would like uh to look into. And with that, I guess we have already boosted our rocket a little bit. We probably won't reach yet the end of the but we also have still have some time to Five will be number will be the next number we will tackle. Custom test extensions. Why on earth would we need something like custom test extensions?
Let's look for an opportunity to throw them in the mix. So, what I have here is a controller test, you know, with mocking and MockMvc, so we test the controller. uh I assume most of you have used MockMvc too in Spring. And when we look at this piece of code, we already might guess that most probably such a header, for instance, will be used in many places
because our application always expects certain headers. So, let's figure that out in our particular application. You see that this particular snippet is just copied all over and over and over again, which, well, is not really ideal. So, it would be nice if we could kind of come up with something that, well, re- um removes this redundancy. We can do this in Java. There is a solution for
that. We could create things like perform and get response with header helper methods, right? We could do that. Um is it a perfect solution? Uh I don't know. Um first of all, we would have to create a lot of overloaded methods because we don't have default arguments in Java. before you know it, you might run into you might run into a conflicting arguments. For instance, if I
would also like to have the next um attribute position two, it would conflict with token. Uh in order to solve that, probably I just have to rearrange arguments, which gives you really a messy API. So, yes, you can kind of do it, but you're probably not going to be very happy with it. And of course, we want to be happy developers, right? Developer experience is important. So,
let's see um if Kotlin can help in this area. Kotlin extensions. Who has used Kotlin extensions? Cool. Most of you. Very good. It's kind of interesting. When I talk about Kotlin extensions to Java developers who's never seen them, they're very skeptical. They think, "Ooh, what is this? This really makes stuff messy. Oh, this is dangerous." And that's fine, right? I can imagine, that's kind of a hard
thing to to grasp. However, I promise you, once you program Kotlin for about 3 weeks, you're going to love them. So, don't be too hard on them judging when you see them for the first time. So, what are extensions? Extensions uh is a mechanism to add new methods to existing objects. That's an extension. And the syntax is very simple. So, we simply define a type you want
to extend, like here local date time, then we just add a method name and optional and then we can use in this particular method this to then do our thing, which refers to the instance of the type we extend here. Once we bring this extension into scope, and this is very important, so extensions are not global, you really have to kind of bring them in scope at
the place where you want to uh use them by means of an import, I all of a sudden have now a formatted method on my local date time. And the cool thing is, your code completion will find them. Have you ever tried to find a wrapper method in Java in code completion? It's kind of hard. It's kind of hard. So, this kind of really plays nicely with
your IDE and tells you which kind of uh glue code I have available for my uh specific object. And of course, because extensions are just other methods, all the other uh features like um default arguments apply, too. So, let's see what we can accomplish um with this feature using the challenge we have encountered in our previous example. So, what I have done here is I've used a
mock HP servlet request builder, so that's one of the intermediate objects that is used by a mock MVC. Um I've used this one to just define my own helper methods like authorization header, like correlation ID header, or maybe something like a default header which do default arguments, whatever. Just what helps you to make your code much more uh aligned with uh the the the section or with
the paradigms you're using in your own application. And then, with that, I simply can say, as you can see, code completion is there. It's there, so it has picked it up. I can say either authorization header. If I don't know this one, I can say default header, and then use again my default argument to change my token. Just what you like, what you would like to do.
And you can imagine that the last section here, so the content type and content, is maybe also a piece um we would repeat over and over again, especially for our puts and posts. So, maybe also for this we can create an Let's do that right away. So, here we have our JSON content extension, which, well, to create JSON uses another extension on a generic type, that's also
possible, right? It just can say T to JSON, and it creates JSON of an object. So, I could use also use this to JSON in various other contexts. And by doing so, we can now replace a lot of redundant code with nice extensions that read nice, are safe, if something needs to be changed, you change it in one spot, and so on. It's all the benefits you
have of code reuse. Um very closely related to extensions are basically serialization utils, utilities. As you might know, the Java um the JVM suffers, I don't know if it's suffering, and has some limitations when it comes to types. Generics only live in the source code. At run time, they're gone because of type erasure. At run time, types are erased. And that is reason foremost for performance. So,
you want to have JVM to be performant, so that's why they don't kind of carry the whole type system into the run time. So, it's just a compile time thing we have. And to work around that, you get kind of a bit clumsy um challenges that are solved as follows. So, if I try to deserialize something, I need to provide, of course, a type to which I
want to have serialized. And because types are not there anymore at run time, we need to provide classes or even uh these these kind of fancy wrappers, parameterized type reference with standard generics, so that we still can kind of figure out to what we need to serialize our final um um input. Kotlin has solved this particular because in Kotlin, if you would do the exact same thing,
you simply would indeed provide the type. Read value, planet. Body, list planet. And it just works. But how does this work? Because you have type erasure on a JVM. Well, Kotlin came up with a trick, and it has a bit of a fancy name, it's called a reified type. And what it basically does is at the call site, so where read value is called, we know always
the type. The compiler knows always the type at this particular location. So, what the compiler does now is it preserves the type information in the byte code. That's basically what reified does. And because um it does that, we now can have constructs like give me the type, give me the class of a certain type. And as such, we do not have to depend on these kind of
constructs. I said, it might sound a little It might sound a bit fancy, reified, but don't worry too much. Um if the compiler figures that you want to have a class of a type, it will tell you, "Hey, I can do that because it needs to be reified." IntelliJ tells me, "Shall I make it reified?" And you just say yes, and that's the way it works. So,
you don't need to know too much about it. Even so, it's kind of fun to look in the byte code what it uh really And this mechanism we now also can use for the last challenge we are trying to solve here. So, what we have here on the left is basically this whole construct, so it's quite a lot of lines of code simply to deserialize something out
of a mock MVC. And by using reification, we can solve this problem now in a very elegant way. Also using extensions, so we have here now a read body extension on a MVC result, which, as you can see, is I said, if it wouldn't be reified, you get a compilation error, so no worries there. I even can include another matcher here, so if something would be null,
I get a decent message, "Yeah, I expected this um to have value, but now it is null." >> [snorts] >> And by doing so, we simply now can replace this whole thing with a read body, and off we go. And you see I need to import extensions. You see uh that was just an import because I said they are scoped, so they're not just everywhere. And by
doing so, well, I got rid of a lot of useless lines of code that are just in my way. I'm not really interested in that. All right. How are we doing? Well, I think we added another booster to our rocket. But if you want to reach the end of the universe, you need, of course, at least two boosters. And for this, we going to go into more
complex realms. the test data we've created so far was kind of manageable, right? So, we used these methods, we used maybe a data class to do some copying, and it kind of worked, I would say, for the use cases we had at hand. But imagine you have a serious domain with a serious amount of um domain classes, things might maybe break down with the approaches we have
used so far. So, imagine we have now a test which wants to test multiple talks. And for multiple talks, I need to define a primary speaker. I need to define co-speakers, maybe another primary speaker. I need to define tags. Um then I have the talk itself, and the talk also need to assemble with my speaker object I just created. So, there's a lot of assembly also taking
place. Then I probably also need to persist stuff, and for this I've created a helper method, persist graph, because a lot of objects in which you need to be persisted. I cannot just give it to a to an ORM like JPA, and then it persists everything. No, because the relationships of a tag, for instance, is not really bound to a talk, so we have to persist them
individually. So, there's this quite a lot of stuff going on here simply to create two talks. And I said, with just the method approach we used before, it's probably like slightly better, but still not ideal. Well, if you have this particular challenge, Kotlin has an advanced feature for you, which are type-safe builders. So, who of you has created a type-safe builder? One. Wow, that's not many. So,
also I guess a majority will learn something What are type-safe builders? Well, just look at them, how they work. a little disclaimer, I could spend a whole session only on type-safe It's an advanced subject. Today, I'd like to keep it on a conceptual level, so you know what it's for, and look a little bit in implementation details, so you get an idea, right? But don't expect to
fully grasp it after uh this short interaction. So, the way this works is you have also uh at the core some builder objects. I call them here DSL, which have some mutable state. And then we have a build method that returns something immutable. That's at least how the builder paradigm should be applied in Kotlin. You have some immutability in builders, but the final result should be immutable
because we like to embrace immutability. Now comes, well, this part here, which which has this fancy argument, speaker DSL.parents. It looks a bit like a lambda, but it's not. It looks a bit like extension methods. It's kind of a weird thing, In the byte code, it is just an it's just another lambda. It's really a lambda. There's no difference between a normal lambda and this. But in
the source code, it has some implications. So, let's see what the implication is because the result we get with this particular setup is this, right? So, we can say speaker, curly brace, name, email. How does this work? It works as follows. In this code block, we actually get the instance of speaker DSL we have created here. So, here we create an instance of the speaker DSL. And
this instance is implicitly available in this scope. So, that's why I can call this.name, this.email, and so on. Then when I call build, this block here will be executed. So, then literally these attributes would be And then with calling build, I would get my speaker as a result. Okay. So far so good. You might think, "Okay, it looks very fancy, but is it so much different from
Java builders?" Not yet, but we go one step further. The cool thing is that you now can kind of orchestrate those in a hierarchical manner. So, now we also create a speakers DSL, so for multiple speakers, which has a list of speakers. We have actually almost the same method, but instead of just returning a speaker, assign the resulting instance to the speakers collection. And then at the
end, we simply return an immutable representation of speakers, an immutable list. And this is what we get We have our speakers on top level, and then the individual speakers within, gives us, first of all, a nice way to structure data, but it will auto automatically assemble it for me. Right? Before we had to kind of pass the speakers to the talks and so on. Now it's just
assembled by the structure we define. That's the benefit we get with this approach. And the result is just another list of speakers. So, let's see how this would turn out if use this particular approach for our talks. This is a DSL I've created for And you see it's very readable. I have talks, then I have talk, then I have all these individual aspects. It's type safe, so
I cannot just create wrong elements in the wrong structure. I cannot create a speaker within a speaker or a speaker within a code speaker. So, the structure is enforced with type safe in in type safe approach. Which gives me a great way to enforce a certain hierarchical structure. The question, of course, is um why would you not do this all the time? The reason is that it
comes with some boilerplate. We've seen that. You need to create such a DSL. AI can help with that, but nevertheless, it is a bit of boilerplate you need to preserve. So, personally, I use this approach foremost when I have complex nested data structure that also require a lot of assembly. For these particular use cases, this is really a killer feature. And for these features, we also use
them. Almost there. Coroutines. Who of you has used coroutines? Ah, many more. Nice, nice. >> [sighs] >> Well, what on earth can coroutines help make our for Java-related code simpler. Well, let's ask another question. Who of you has used Reactor? Who of you liked to use Reactor? Ah, two people. Ah, wow. Amazing. Great. You're awesome. Um I understand that the Reactor is great because it allows you
to do things in parallel. Let's Let's look at the example here. So, I have here an engagement service. It takes some engagement um update request, which then uh what it basically does is updates some metric system, which has some uh which requires a remote call for a view of a talk, a like of a talk, and whether someone is attending it or not. And we want to
all do this in parallel, because we don't just want to call all these uh calls um sequentially. That would take too long. We want to do this simultaneously. In order to do that, we need to take refuge to reactive building blocks. Monos, that's the only thing you have right now in Java. Virtual threads won't solve this problem. There's no uh parallelism in virtual thread yet. Structured concurrency,
maybe years from now you can can be done. But today, this is the only way in Java you can execute stuff in parallel. Could use a completable future, but the kind of the um the complexity involved with it is more or less the same. Also here, when I when I want to get an engagement, I also want to fetch the status simultaneously. For this, I need to
take um zip into account. Which is probably not very straightforward, right? Zip means doing stuff in parallel. Yeah, when you know, you know it, but when you see for the first time, you think, "What is zip doing?" However, this was just the beginning. Now we try to test this code. This is where actually the headache really starts. You need to take and embrace the step verifier. Who
has used step verifier? The ones who went really deep into the the the caves. Yeah. But probably it didn't it didn't you didn't master it overnight. It took some effort to master this particular building block. It's very complex. It has a first of all, complex abstraction. You really need to know how these monos work. So, here we create actually two record engagements simultaneously using zip. Then accumulating
results is quite complex, right? Because the call chain you do create, and then flat map, map, and then you get actually results of the previous operations with T1, T2, and current. And then um finally, we are in a certain next, and then I can get the results accumulated here out, which is get two. What is get two? It's probably current, I hope. And then uh last but
not least, if I uh forget to call verify complete, your test will be green, but nothing has happened. So, there are really myriad ways to shoot yourself in the foot. Coroutines um are really a very elegant solution to this problem. The problem with actual reactive building block is that they impose a complex abstraction upon you with complex operators like flat map map and This is why reactive
programming is so hard. It's not about your intent you express. It's all about these monos, these building blocks. Um coroutines give you the best of both worlds. They convert such a mono into something called a suspend method, basically is a built-in reactive capability in the Kotlin language. And from there on, it's just sequential code. And only thing you need is something like run test or run blocking
to give you a certain context to make that available. And when we look to see how this works in practice, we should be able to do really a lot of So, we need this run test wrapper here. The ramp up is more or less the same like you see here on the left. I have just my engagements. Then now I try to create the engagement simultaneously. I
say engagement.map. And now I get a list of monos. But I don't want to have monos. I want to have a list of items, right? Monos go away. I simply say await all. >> [applause and cheering] >> The same is true for our get current engagement. Now we get the values. We also would get a mono. That's what you see here. And by simply saying uh await
single, we would get the desired result. And as such, assertions is just another call with our code test library to do its thing. That's what we see here. We just reduce to additional matches to to match the result. So, there is no reactivity going on anymore here. Even though this fully behaves reactive. So, there's no blocking going on. It's fully reactive execution, which also is kind of
um clearly visible when we try to debug stuff. It's just like debugging blocking code. There is no difference with that. You see, I just can do my traditional debugging as I did before. So, if you have to test reactive code in Java, then definitely coroutines is something you should take a look at. Okay. And with that, um we've actually uh boosted our rocket. Hopefully, I have 2
minutes left, so let's speed things a bit up. What you also can do in Kotlin, which is really cool, is exploratory testing with Kotlin notebooks. Has anybody used notebooks? Okay. Not many. So, the way this works is I have my experiment application running. It is a kind of worksheet which has a markdown section and code sections. And now I just can execute them. So, here I create
a REST client. And now I can call my REST API, and it gives me results. But it's much cooler than that. I can execute this section here and get actually all the Spring beans in my application at my fingertips. Can you imagine that? So, if I would say the speaker service in which code completion, my Spring bean is just available here as a code completion. And I
can directly interact with this particular bean like tag service create tag. There it is. Let's get all the tags. There they are. Let's call the endpoint again. Wow. And let's just recreate the test data, so I have my tag repository. Okay. Um not find all, but delete all. There we go. Let's execute that. Now they're gone. So, this is a very great way to maybe debug or
play around with the application and find cases you haven't tested yet before. Okay. And with that, I guess um we are ready for launch. Um the result so far is that we have about 30% less code, lines of code. But of course, it's not about lines of code, right? It's about that we have now maximal reuse. It's about that we have removed a lot of boilerplate. It's
about that we have uh simplified our It's about that we made our test suite much more concise and maintainable. Which, on this planet we have landed, is not only nice for you as human, but also for AI, because AI really loves a structured, noise-free uh base on which it can continue. And these are just other nice benefits, less tokens, less money and reuse. I hear you say,
"Uh sorry, too much effort for me." Well, I have just yesterday, to be honest, in my hotel room, I've created some nice skills for you that actually allow you to convert a piece of Java code with the exact thing we just have done today. I've created these skills in about an hour. You can use them in They're on my repository. Um if I would have time left,
I could show even a film of you. You just say skill, convert my Java code to these best practices, and it just does it for you. 95% correct. Then of course, you need to correct maybe some minor things, but it's just done for you. So, thinking it's too much effort to get this argument doesn't hold anymore. And this is in the end what you get. More fun
to write and read tests. May Kotlin be with you. Yes. >> [applause]
More from this event
See all 38 talks →
Spring I/O 2026 Keynote
1:08:44
The Spring AI Ecosystem in 2026: From Foundations to Agents @ Spring I/O 2026
43:39
Breaching LLM-Powered Applications: Overcoming Security and Privacy Challenges by Brian Vermeer
48:40
New in Spring Security 7: MFA, OAuth2 and more by Daniel Garnier @ Spring I/O 2026
46:43