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 in production, 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 in production.
- 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 in production 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 an application, but very helpful for education and testing in production.
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 in production 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:
- If user attribute
testAttribute
(string) matchesAAA
serveevery
. - If user attribute
testAttribute
(string) matchesBBB
servenone
- If user attribute
testAttribute
(string) matchesCCC
servenon-test
- If user attribute
testAttribute
(string) matchesDDD
servetest-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!
Get Split Certified
Split Arcade includes product explainer videos, clickable product tutorials, manipulatable code examples, and interactive challenges.
Deliver Features That Matter, Faster. And Exhale.
Split is a feature management platform that attributes insightful data to everything you release. Whether your team is looking to test in production, perform gradual rollouts, or experiment with new features–Split ensures your efforts are safe, visible, and highly impactful. What a Release. Get going with a free account, schedule a demo to learn more, or contact us for further questions and support.