Testing in Production with Spring Boot and Kotlin

In this tutorial, you are going to build a Spring Boot REST application using Kotlin. Your resource server will use Spring Data to map a Kotlin class to a database table using simple JPA annotations. You will also use Spring Security to add HTTP Basic authentication and method-level authorization to the HTTP endpoints. Your Spring MVC-powered resource server will allow for the creation, deletion, update, and listing of data object instances. Finally, you will use Split’s Java SDK to implement a feature flag that will allow you to dynamically update app behavior based on user attributes.

You may at this point be asking a few questions. Why Kotlin? I thought Spring was Java. What is Kotlin and why use it? Or, perhaps, what is a feature flag? Why would I want to use a feature flag in my code?

Kotlin is a JVM-based language built by JetBrains (the people who make IntelliJ). Java compiles down to an intermediate bytecode that runs on the Java Virtual Machine (JVM). This is what allows Java to be platform interdependent while still retaining a lot of the performance benefits of compiled code. Kotlin, which you can think of as Java re-imagined, also compiles down to the same byte-code. This means that Kotlin and Java are totally interoperable. Kotlin classes can depend on and use Java libraries. You can mix and match Java and Kotlin classes in the same project if you want.

So why bother with Kotlin at all? Java is a great language, but it has a tendency to be long-winded with lots of ceremony code. Further, the Java syntax was developed before the functional coding paradigm emerged, and Java can only change so much because of all the institutional success it has had.

The last decade has seen an amazing amount of growth in programming language development, from modern Javascript to more niche languages like Go and Scala. Kotlin is an outgrowth of this development. It merges a lot of functional coding ability and syntactic simplicity of scripting languages with a Java-esque syntax to create a concise, powerful coding language. It also adds some incredibly useful features such as built-in null-pointer checking syntax.

Now, what about feature flags? Feature flags are a way of controlling application behavior dynamically, at run-time. They are flags, or variables, that can be used in your application code whose value can be updated in real-time. Split provides a powerful, software-as-service implementation of a feature flag system.

Feature flags can be used for testing, to carefully introduce a new feature and roll it back if it doesn’t work, or expand its deployment if it does — all without having to recompile and redeploy. Feature flags can also be used to segment features based on things like location or subscription level. There are endless possible uses for feature flags. Ultimately, one of the main uses is to decouple changes in code behavior from the need to deploy code. Split also has an extensive metrics and event monitoring API that can be used to track application use and customer behavior.

In this tutorial, you will be building and serving imaginary paper airplane designs. Each paper airplane design will have a number of folds it takes to make the paper airplane as well as a name. Further, the paper airplane will have a boolean value named isTesting that will be the flag you will use to demonstrate the feature flags. You will be able to “serve” paper airplane specifications via the REST interface, and the server will be able to create, read, update, and delete paper airplane designs from the database.

Dependencies

Java: This tutorial uses Java 11. You can download and install Java by going to the AdaptOpenJdk website. Or you can use a version manager like SDKMAN or even Homebrew.

Split: Sign up for a free Split account if you don’t already have one. This is how you’ll implement the feature flags.

HTTPie: This is a powerful command-line HTTP request utility you’ll use to test the reactive server. Install it according to the docs on their site.

Bootstrap the App with Spring Initializr

You will use the Spring Initializr project to download a pre-configured starter project. Open this link and click the Generate button. Download the demo.zip file and unzip it somewhere on your local computer.

You’re downloading a Spring Boot project that is configured to use Gradle as the build system, Kotlin as the main project language, and Java version 11.

There are four dependencies included as well:

  • Spring Web: adds basic HTML and REST capabilities.
  • Spring Data JPA: allows the app to map class objects to database tables using annotations.
  • H2 Database: provides a simple, in-memory database that is perfect for demo projects and testing.
  • Spring Security: includes Spring security functions that will allow the app to include HTTP Basic authentication.

Create a Secure CRUD App

The bootstrapped app you just downloaded doesn’t do much yet. It’s just the project skeleton. The first step is to implement a CRUD (Create, Read, Update, and Delete) application. You’ll do this entirely in Kotlin. As mentioned, the application will be secured using Spring Security and HTTP Basic.

Replace the contents of the DemoApplication.kt file with the code below.

src/main/kotlin/com/example/demo/DemoApplication.kt

package com.example.demo import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean @SpringBootApplication class DemoApplication { // Initialize some sample data @Bean fun init(repository: PaperAirplaneRepository): ApplicationRunner { return ApplicationRunner { _: ApplicationArguments? -> var paperAirplane = PaperAirplane(null, "Bi-Fold", 2, false); repository.save(paperAirplane); paperAirplane = PaperAirplane(null, "Tri-Fold", 3, false); repository.save(paperAirplane); paperAirplane = PaperAirplane(null, "Big-ol-wad", 100, true); repository.save(paperAirplane); } } } fun main(args: Array<String>) { runApplication<DemoApplication>(*args) }
Code language: Java (java)

This file is the main entry point into the Spring Boot application, via the main() method at the bottom of the file, which runs the DemoApplication class that is annotated with @SpringBootApplication. Within that class is an initialization function that loads three paper airplane designs into the paper airplane repository as the application loads. This is so that the application has some data to demonstrate its function. The class PaperAirplaneRepository is a class you’ll define in a moment.

Next create a SecurityConfiguration class.

src/main/kotlin/com/example/demo/SecurityConfiguration.kt

package com.example.demo import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.provisioning.InMemoryUserDetailsManager @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class SecurityConfiguration : WebSecurityConfigurerAdapter() { // Disable CORS and enable HTTP Basic override fun configure(http: HttpSecurity) { http .cors().and().csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); } // Create two users: admin and user @Bean fun users(): UserDetailsService { val user = User.builder() .username("user") .password("{noop}user") .roles("USER") .build() val admin = User.builder() .username("admin") .password("{noop}admin") .roles("USER", "ADMIN") .build() val test = User.builder() .username("test") .password("{noop}test") .roles("USER", "TEST") .build() return InMemoryUserDetailsManager(user, admin, test) } }
Code language: Swift (swift)

This class configures most of the Spring Security options for the application. In the configure(http: HttpSecurity) function, it disables CORS and Cross-Site Request Forgery detection while also enabling HTTP Basic authentication for all requests. You do this to simplify testing on your computer.

The UserDetailsService bean configures three users using an in-memory user manager: user, admin, and test. The {noop} in the password param just tells Spring Boot to save the user password in plain text. It is not part of the actual user password.

All of this is only for testing and demonstration purposes and is not at all ready for prime time. In a production application, you should not be disabling CORS and CSRF, you should not be using HTTP Basic, and you should not be using hard-coded, in-memory user credentials.

Two annotations are used to help enable security on the Spring Boot application.

@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true)
Code language: Elixir (elixir)

The annotation @EnableWebSecurity tells Spring Boot to look in this class for the configure(http: HttpSecurity) function, which is where our basic security options are configured. The second annotation, @EnableGlobalMethodSecurity(prePostEnabled = true) enables method-level security via the @PreAuthorize annotation, which as you’ll see demonstrated in a moment, allows you to move the method-level authorization logic from this configuration function to the controller methods themselves.

Create a data model file called PaperAirplane.kt.

src/main/kotlin/com/example/demo/PaperAirplane.kt

package com.example.demo import org.springframework.data.repository.CrudRepository import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.Id // Defines the data model @Entity data class PaperAirplane( @GeneratedValue @Id var id: Long? = null, var name: String, var folds: Int, var isTesting: Boolean = true ) // Enables persistence of PaperAirplane entity data model interface PaperAirplaneRepository : CrudRepository<PaperAirplane, Long> { fun findByName(name: String): PaperAirplane? fun findByIsTesting(value: Boolean): List<PaperAirplane>? }
Code language: Java (java)

This class defines both an entity and a repository. The entity, marked by the @Entity annotation, is what defines the data model. The data model has four properties, including one auto-generated id parameter that will be mapped to the id column in a database table. The other properties define the parameters of our paper airplane instances. The last property, isTesting , is the property we will use in the feature flag later. It marks the paper airplane design as being in testing.

In the application class, you will remember, three paper airplane designs are initialized: a bi-fold, a tri-fold, and a super-secret “big-ol-wad” with a thousand folds that is still in testing. In this tutorial, imagine that the first two designs are already deployed in production and the third is still in testing. First, directly below, you will see how to implement a secure resource server that controls access based on user roles. After that, you will use feature flags to control the rollout of the test design to the test user before deploying it to all users (simulated here by user).

The repository, a subclass of Spring Boot’s CrudRepository, is what handles mapping the data model class to the database table. It comes with some standard persistence functions for creating, deleting, retrieving, and updating persisted entities (take a look at the API docs).

This interface can also be extended using a natural language API. This is what is used in the two methods contained within the interface: findByName and findByIsTesting, which add the ability to query by name and isTesting. Notice how no actual implementation has to be provided. Simply defining the method name according to the query language exposes the desired functionality. The Spring docs do a good job of covering the query language.

Finally, add a controller class named PaperAirplaneController.

src/main/kotlin/com/example/demo/PaperAirplaneController.kt

package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import java.security.Principal import java.util.* @RestController class PaperAirplaneController(val repository: PaperAirplaneRepository) { @GetMapping("principal") fun info(principal: Principal): String { return principal.toString(); } @GetMapping @PreAuthorize("hasAuthority('ROLE_USER')") fun index(principal: Principal): List<PaperAirplane>? { return repository.findAll().toList() }; @PostMapping @PreAuthorize("hasAuthority('ROLE_ADMIN')") fun post(@RequestParam name: String, @RequestParam folds: Int, @RequestParam isTesting: Boolean, principal: Principal): PaperAirplane { val paperAirplane = PaperAirplane(null, name, folds, isTesting); repository.save(paperAirplane); return paperAirplane; } @PreAuthorize("hasAuthority('ROLE_ADMIN')") @PutMapping fun post(@RequestParam name: String, @RequestParam folds: Int, @RequestParam id: Long, @RequestParam isTesting: Boolean, principal: Principal): ResponseEntity<PaperAirplane> { val paperAirplane: Optional<PaperAirplane> = repository.findById(id) return if (paperAirplane.isPresent) { paperAirplane.get().name = name paperAirplane.get().folds = folds paperAirplane.get().isTesting = isTesting repository.save(paperAirplane.get()) ResponseEntity.ok(paperAirplane.get()) } else { ResponseEntity.notFound().build() } } @PreAuthorize("hasAuthority('ROLE_ADMIN')") @DeleteMapping fun delete(@RequestParam id: Long, principal: Principal): ResponseEntity<String> { return if (repository.existsById(id)) { val response = repository.deleteById(id); print(response); ResponseEntity.ok("Deleted"); } else { ResponseEntity.notFound().build(); } } }
Code language: Scala (scala)

This controller class defines the HTTP endpoints for the REST interface. Notice that the PaperAirplaneRepository repository is included in the class using Spring’s dependency injection in the class constructor. The controller includes mappings for HTTP POST, GET, PUT, and DELETE (create, read, update, and delete).

The @PreAuthorize annotation is used to restrict the GET method to authorized users with the role USER and restrict the PUT, POST, and DELETE methods to users with the role ADMIN.

There is also a /principal endpoint that has nothing to do with the CRUD methods for paper airplanes but instead will return the text of the Principal.toString() method. The Principal object is what represents the authenticated user and it can be educational and helpful to see what this contains. Again, this is not something you would have in a production application, but very helpful for education and testing.

Test the Secure App

Run the application by opening a Bash shell at the project root and running the following command.

./gradlew bootRun

You should see output that ends in something like the following.

2021-10-20 17:39:45.057 INFO 160959 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-10-20 17:39:45.064 INFO 160959 --- [ main] com.example.demo.DemoApplicationKt : Started DemoApplicationKt in 2.335 seconds (JVM running for 2.581) 2021-10-20 17:39:48.050 INFO 160959 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2021-10-20 17:39:48.050 INFO 160959 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2021-10-20 17:39:48.051 INFO 160959 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
Code language: Python (python)

Open a second Bash shell and use HTTPie to run a GET on the home endpoint.

http :8080
Code language: CSS (css)

You’ll see 401 Unathorized error. This is expected because you didn’t provide any user credentials.

Try it again using the first user (user).

http -a user:user :8080
Code language: CSS (css)

This time you’ll get a response that contains the three paper airplanes defined in the initialization function.

HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" }, { "folds": 100, "id": 3, "isTesting": true, "name": "Big-ol-wad" } ]
Code language: Bash (bash)

Take a look at the /principal endpoint.

http -a user:user:8080/principal

You will see output like the following. This is the text from the Principal.toString() method. The Principal object in Spring represents the authenticated user, and it can be helpful to see what kind of properties it contains. The last entry, Granted Authorities, is particularly helpful as this is what the @PreAuthorize annotations will look for when the SpEL (Spring Expression Language) snippet hasAuthority is used. You can see that user has the granted authority ROLE_USER. This authority is automatically generated because when the user was created it was assigned to the role user.

HTTP/1.1 200 ... UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_USER]]
Code language: PHP (php)

If you use the same endpoint to look at the admin user you’ll see that admin has both ROLE_USER and ROLE_ADMIN authorities, which is why you can use the admin user on the POST, PUT, and DELETE methods protected by @PreAuthorize("hasAuthority('ROLE_USER')").

http -a admin:admin POST :8080/info
HTTP/1.1 200 ... UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN, ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ADMIN, ROLE_USER]]
Code language: PHP (php)

Use the admin user to add a fourth paper airplane via the POST method.

http -a admin:admin POST :8080 name==Testing folds==4 isTesting==true
Code language: Bash (bash)
HTTP/1.1 200 ... { "folds": 4, "id": 4, "isTesting": true, "name": "Testing" }
Code language: Bash (bash)

Notice the double equals signs in the parameters in the command above. This is not a mistake but is how HTTPie marks parameters to be included as query string parameters, which is what our Spring Boot methods are expecting because they use the @RequestParam annotations.

You can get the home endpoint again, using either user, and verify that the new paper airplane has been added.

http -a user:user :8080
Code language: CSS (css)
HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" }, { "folds": 100, "id": 3, "isTesting": true, "name": "Big-ol-wad" }, { "folds": 4, "id": 4, "isTesting": true, "name": "Testing" } ]
Code language: Bash (bash)

You can also delete paper airplanes.

http -a admin:admin DELETE :8080 id==1
Code language: SQL (Structured Query Language) (sql)
HTTP/1.1 200 ... Deleted

And update them (using PUT). The command below changes the isTesting value on the Tri-Fold paper airplane from false to true.

http -a admin:admin -f PUT :8080 name=Tri-Fold folds=3 isTesting=true id=2
Code language: Bash (bash)
HTTP/1.1 200 ... { "folds": 3, "id": 2, "isTesting": true, "name": "Tri-Fold" }
Code language: Bash (bash)

Notice that PUT requires that you send values for all of the data model properties and uses the id value as the key. You cannot just send values for the property you want to update.

Finally, you can verify that user cannot access the admin methods.

http -a user:user DELETE :8080 id==1
Code language: SQL (Structured Query Language) (sql)
HTTP/1.1 403 ... { "error": "Forbidden", "path": "/", "status": 403, "timestamp": "2021-10-21T01:04:16.232+00:00" }
Code language: Bash (bash)

At this point, you have a secure (for our demonstration purposes) application with full CRUD functionality. The data model objects, the paper airplanes, are being automatically mapped from the Kotlin entity class to the H2 in-memory database. Because the database is an in-memory database, it’s getting erased every time you stop and restart the application. This is actually handy for testing and demonstration purposes but in a production scenario, you’d want to connect this database to something that persists between sessions, such as an SQL or MySQL database.

Create the Treatment on Split

Now you’re going to take a break from coding for a moment and create your treatment on Split. You can think of a treatment as a decision point in the code. It’s also known as a split. It’s a point of logic in the code that can be used to determine application behavior. A treatment is a specific value that the split can take.

For example, you’re going to create a simple boolean split that can have two treatments: on and off. However, as you’ll see later, splits do not need to be boolean and can have any number of values encoded by strings, so really, it’s up to you as to how you use them.

Splits are a little like the very old-school way of using boolean flags to control hard-coded options in code. However, they are far more powerful. Because the feature flags are hosted on Split’s servers, they can be updated in real-time. Thus feature flags allow you to modify application behavior in real-time via Split’s control panel.

Further, you can create very advanced segments, or specific populations of users, that have specific treatment states. Split gives you great flexibility in how you define treatment states and segment populations.

Using this technology, it is possible, for example, to roll out a set of test features to a small subset of users. If the test goes well, you can then roll the features out to more users. If the test does not go well, you can roll back the new features. The best part is: you can do all of this without compiling or deploying any code.

In this application, you’re going to pretend that one of the paper airplane designs is still in testing, the big-ol-wad with a 1000 folds (this is just a big wadded up ball, but don’t tell anyone, it’s a trade secret). You will use the split to initially roll out this test feature only to the test user. Once this test is successful, you will update the split and see how you can dynamically roll the new design out to all the users.

Practically speaking, in the code, you’ll use the Split Client to send the user information to the Split servers and retrieve the treatment (on or off state of the split), which you’ll use to determine application behavior.

To create the split, log into your Split developer dashboard.

Click the Create Split button.

Make sure the Environment at the top says Staging-Default. This must match the API keys you use in the Split Client later.

Give the split a name: isTesting Select the Traffic Type user. Click Create.

The isTesting split page should already be shown. If not, go to Splits and select the isTesting split in the list of splits.

Click Add Rules to add targeting rules for the split. Targeting rules are how the split determines what state it returns for a given query by the Split Client.

Under the Set targeting rules heading, click Add rule. Click on the drop-down that says is in segment and select string and is in list. Type test in the adjacent text field. Change the serve value to on.

Click Save changes at the top of the panel. Click Confirm.

That’s it. You created the split. Now you need to add the Split Client to the application.

Integrate the Split Client in the App

The first thing is to add the Split dependency to the build.gradle file.

dependencies { ... implementation("io.split.client:java-client:4.2.1") }
Code language: Bash (bash)

Next, create a SplitConfiguration Kotlin class that will handle configuring the Split Client and creating the Spring Bean so that it will be available for dependency injection. This represents current best practices on how to configure and use the Split Client in Spring Boot for most applications.

src/main/kotlin/com/example/demo/SplitConfiguration.kt

package com.example.demo import io.split.client.SplitClient import io.split.client.SplitClientConfig import io.split.client.SplitFactoryBuilder import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class SplitConfig { @Value("\${split.io.api.key}") private val splitApiKey: String? = null @Bean @Throws(Exception::class) fun splitClient(): SplitClient { val config = SplitClientConfig.builder() .setBlockUntilReadyTimeout(10000) .enableDebug() .build() val splitFactory = SplitFactoryBuilder.build(splitApiKey, config) val client = splitFactory.client() client.blockUntilReady() return client } }
Code language: Scala (scala)

You need to add your Split API key to your application.properties file. The API key you add here must be from the same environment in which you created the split above (Staging-Default, if you followed the instructions).

To find your API key, open your Split developer dashboard.

Click on the icon in the upper left that says DE (for “Default” workspace).

Click on Admin settings and then API keys.

Keys are divided by both type and environment. There are client-side keys and server-side keys. There may also be keys for each environment, Staging-Default and Production-Default, if you are using the default environments created for you.

You want to copy the server-side and Staging-Default key. Click the Copy link. You are using the server-side key because you are using a server-side technology (Java and Kotlin). If you were creating a Javascript-based client application with a Vue or React front end, you would need to use the client-side keys for the client application.

Open src/main/resources/application.properties and add the following line, replacing {yourApiKey} with your actual API key.

split.io.api.key={yourApiKey}
Code language: Swift (swift)

To use the Split Client you need to use Spring’s dependency injection to make the bean available. Just like was done with the PaperAirplaneRepository, you can add it to the class constructor. Modify the class constructor by adding the Split Client, as shown below. Also add the import statement.

import io.split.client.SplitClient ... class PaperAirplaneController(val repository: PaperAirplaneRepository, val splitClient: SplitClient) { ... }
Code language: Swift (swift)

Next, update the GET method to use the Split Client.

@GetMapping @PreAuthorize("hasAuthority('ROLE_USER')") fun index(principal: Principal): List<PaperAirplane>? { val treatment: String = splitClient.getTreatment(principal.name, "isTesting"); println("username=${principal.name}, treatment=$treatment") return if (treatment == "on") { repository.findAll().toList() } else { repository.findByIsTesting(false) } }
Code language: Java (java)

In the method above, the treatment state is being used to determine if all of the paper airplanes are going to be returned (treatment state equals on), including the “test” paper airplanes; or, alternatively, if the treatment state is off, only the non-test paper airplanes will be returned.

The getTreatment() method is what returns the treatment state that determines the application behavior. This is the feature flag or split in the code. The method takes two values: the username and the name of the split. This method can also take an array of user attributes, so any arbitrary user data can be used to generate treatments, not just the username.

Because the test user was added to the targeting rule list, the test user should return the on treatment while the user user should return the off treatment.

The getTreatment() method is very fast, requiring only milliseconds. This is because treatment values are cached and updated asynchronously behind the scenes. Therefore when the getTreatment() method is called, it’s generally not actually making a slow network request but just returning the appropriate cached value. However, these values are still updated in near real-time.

Test the Split

Now you can test the split. You can make the GET request with the admin and user users and see how the application will return different values based on the returned treatments.

Stop your app if it is still running using control-c and start it again.

./.gradlew bootRun

First, (in a different Bash shell) try user.

http -a user:user :8080
Code language: CSS (css)

You’ll only get the non-test paper airplanes.

HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" } ]
Code language: Bash (bash)

Now try test.

http -a test:test :8080
Code language: Bash (bash)

All of the paper airplanes are returned.

HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" }, { "folds": 100, "id": 3, "isTesting": true, "name": "Big-ol-wad" } ]
Code language: Bash (bash)

This is a very simple example of how application behavior can be determined using feature flags and split. The first user, user, is not in the targeting rules for the split and so gets served the default treatment, which is off. This value is used in the Kotlin controller class to filter out the test paper airplanes. The test user, however, receives the on treatment, and this is the signal for the controller to return all of the paper airplanes, including the test ones.

Update the Split in Real-Time

Imagine you were testing the test paper airplanes with the test user and they were successful. Now you want to deploy them to all of the users. Using feature flags, you’ll be able to do this dynamically without having to rebuild or redeploy any code.

Go to your Split dashboard. Click Splits and select the isTesting split.

Scroll down to the section Set the default rule. Change this to on. You can also change the section called Set the default treatment so that the default treatment is on as well.

It’s helpful to understand what these two settings configure. The default rule specifies which treatment will be applied if none of the targeting rules specify a treatment. This assumes a successful call to getTreatment() from the Split Client. The default treatment is a fallback treatment that will be assigned in case anything else goes wrong, such as if there’s a network failure or if the split is killed.

The changes to the treatment rules are not live until you have saved and confirmed the changes. Click Save changes at the top of the panel. Click Confirm.

Once you’ve made this update, you can try user again and you will see that all of the paper airplanes will be returned.

http -a user:user :8080
Code language: CSS (css)
HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" }, { "folds": 100, "id": 3, "isTesting": true, "name": "Big-ol-wad" } ]
Code language: Bash (bash)

Update Split to Use User Attributes

As I mentioned, splits don’t have to simply use the username to determine the treatment state. You can send a map of user attributes in the getTreatment() function and these can be used to determine the returned value.

Next, you’ll create a new split and modify the GET function to demonstrate this ability by creating a split with four possible treatment values: every, none, test, and non-test. For simplicity’s sake, the attribute will simply be a value passed in through a query parameter to the GET method.

Change the GET method in the PaperAirplaneController class to match the following.

@GetMapping @PreAuthorize("hasAuthority('ROLE_USER')") fun index(principal: Principal, @RequestParam testAttribute: String): List<PaperAirplane>? { // Create the attribute map val attributes: Map<String, String> = mapOf("testAttribute" to testAttribute) // Pass the attribute to the getTreament() method val treatment: String = splitClient.getTreatment(principal.name, "moreTesting", attributes) println("username=${principal.name}, testAttribute=${testAttribute}, treatment=$treatment") return if (treatment == "every") { repository.findAll().toList() } else if (treatment == "non-test") { repository.findByIsTesting(false) } else if (treatment == "test-only") { repository.findByIsTesting(true) } else { // none return emptyList(); } }
Code language: JavaScript (javascript)

Open your Split dashboard.

From the left menu, select Splits and click the Create split button.

For the split name, call it moreTesting.

Set the Traffic Type to user.

Click Create to create the new split.

Click Add rules in the new split panel.

Under Define treatments, you want to change the treatments so that there are four treatments: every, none, non-test, and test-only. For two of the values you can change the two existing values, but for the other two, add two new treatments using the Add treatment button.

Remember that these treatment values are just string identifiers. What they mean is arbitrary and up to you and how you use them in your application.

Scroll down to Set targeting rules.

Click Add rule four times to add four new rules.

Configure the four rules to read as follows:

  1. If user attribute testAttribute (string) matches AAA serve every.
  2. If user attribute testAttribute (string) matches BBB serve none
  3. If user attribute testAttribute (string) matches CCC serve non-test
  4. If user attribute testAttribute (string) matches DDD serve test-only

In the section Set the default rule, change the default treatment from none to non-test. Also under Set the default treatment, change the value to non-test.

Save the treatment by clicking Save changes at the top of the panel. Click Confirm.

Go back to your Kotlin Spring Boot application. If it’s running, stop it using control-c and start it again.

./gradlew bootRun

Once that has finished loading, in a separate Bash shell, you can make some requests to test the attribute-based splits.

For example, the following will authenticate as user with the testAttribute attribute equal to AAA. Because of the targeting rules you just defined, this will serve the every treatment, which will cause the app to return all of the paper airplanes from the database without any filtering.

http -a user:user :8080 testAttribute==AAA
HTTP/1.1 200 ... [ { "folds": 2, "id": 1, "isTesting": false, "name": "Bi-Fold" }, { "folds": 3, "id": 2, "isTesting": false, "name": "Tri-Fold" }, { "folds": 100, "id": 3, "isTesting": true, "name": "Big-ol-wad" } ]
Code language: Bash (bash)

If you pass in the BBB for the treatment value, you’ll get the none treatment and no paper airplanes.

http -a user:user :8080 testAttribute==BBB
HTTP/1.1 200 ... []
Code language: C# (cs)

Similarly, you can perform requests for the other attribute values. I won’t reproduce the output here but it should be pretty clear that CCC will return only the non-test paper airplanes and DDD will return only the test ones.

http -a user:user :8080 testAttribute==CCC
http -a user:user :8080 testAttribute==DDD

This example is, of course, a little contrived in the sense that typically you wouldn’t be passing in the attributes via query parameters. This example is designed for coding clarity and simplicity in demonstration. Instead, in a real scenario, these attributes would be attributes of your user. They might represent membership in a testing group, geographical location, time as a customer, subscriber level, etc. — basically any attribute on which you want to segment your user population so that you can modify application behavior in real-time.

Wrapping Up

Next steps for this tutorial might be to implement metrics tracking and event monitoring in Split. Arbitrary events can be sent to Split servers via the SplitClient.track() method. This method takes three params: a String key, a traffic type value, and an event type value. Here are a couple of examples from the Split docs.

// If you would like to send an event without a value val trackEvent: boolean = client.track("key", "TRAFFIC_TYPE", "EVENT_TYPE") // Example val trackEvent: boolean = client.track("john@doe.com", "user", "page_load_time")
Code language: Arduino (arduino)

We don’t have time to go into much more detail about events here. Split has the ability to calculate metrics based on event data and even trigger alarms or change treatment states based on the calculated metrics.

In this tutorial, you created a sample resource server with full CRUD capabilities. The server was protected with Spring Security and utilized Spring Data to map a Kotlin class to an in-memory relational database. You used Split’s implementation of feature flags to see how features can be controlled at runtime dynamically without having to redeploy code. You also saw how arbitrary user attributes can be used with the Split API to control the treatment a feature flag returns.

Learn More About Spring Boot

Ready to learn more? We’ve got some additional resources covering all of these topics and more! Check out:

Containerization with Spring Boot and Docker
A Simple Guide to Reactive Java with Spring Webflux
Get Started with Spring Boot and Vue.js
Build a CRUD App in Spring Boot with MongoDB
Reduce Cycle Time with Feature Flags
Feature Monitoring in 15 Minutes

To stay up to date on all things in feature flagging and app building, follow us on Twitter @splitsoftware, and subscribe to our YouTube channel!