Riposte Microservice Template - Java

Code Coverage

This is an example template for quickly creating a new Java-based Riposte project. Riposte is a Netty-based microservice framework for rapid development of production-ready HTTP APIs. It includes robust features baked in like distributed tracing (provided by the Zipkin-compatible Wingtips), error handling and validation (pluggable implementation with the default provided by Backstopper), and circuit breaking (provided by Fastbreak).

IMPORTANT NOTE: Riposte requires a minimum of Java 8. This project will not build or run unless you use a Java 8 or later JDK. Verify you're using a Java 8 or later JDK with a simple java -version. This project is also ready for Java 11 - if you want to use Java 11 see this section of the readme.

Want a Kotlin Version of this Microservice Template?

You're currently viewing the Java version of this Riposte microservice template. A Kotlin version exists - if you want to create a Kotlin-native Riposte project please see the Kotlin branch of this repository.

TL;DR - Getting Started

Table of Contents

Bootstrapping New Projects

Bootstrapping new Riposte projects is straightforward - you can do it in an automated fashion with a one line command and you don't even need to checkout the template project repository. If that doesn't work in your environment you can do the manual method that has a few more steps but is also straightforward.

Automated bootstrapping

Just run the following curl command in a command line shell, replacing and/or removing the <newprojectname>, <myorgname>, </optional/target/dir>, and <-DoptionalSystemProps=stuff> arguments as necessary (arguments and options explained below):

curl -s 'https://raw.githubusercontent.com/Nike-Inc/riposte-microservice-template/master/bootstrap_template.sh' \
| bash /dev/stdin <newprojectname> <myorgname> </optional/target/dir> <-DoptionalSystemProps=stuff>

After you execute the curl command your new project will be setup and ready to use.

Manual bootstrapping

If the curl command above doesn't work for you then you will need to perform a few more steps to setup your project:

When the gradlew command finishes your new project will be setup and ready to use.

Bootstrap command arguments

Setting environment-specific properties during bootstrapping

There are some project-specific config properties in this template project. You can set them up manually after bootstrapping (search for fixme_ in the project), or you can enrich the bootstrapping commands above with extra info (the <-DoptionalSystemProps=stuff> properties described above) and have those properties set at bootstrapping time when the project is first created. This is great for repeatability and rapid iteration.

The following table describes the System Property values you can pass in with the automated or manual bootstrap command for the <-DoptionalSystemProps=stuff> argument(s) and what they do. For each prop_key defined below that you want to specify you would pass in -Dprop_key=value. See the Example all-in-one curl command after this table for a concrete example of these properties in action. All of these are optional - any you don't specify will just have the fixme_* value left in the configs, and since they just relate to Eureka and remote testing in test and prod environments it won't affect your ability to explore, build, or run your project locally.

System Property Key Explanation
fixme_project_remotetest_url_test The URL to your server(s) deployed in the test environment, e.g. https://myproject.test.myorg.com. Used for executing the remote functional tests against the test environment.
fixme_project_remotetest_url_prod Same as above, but for prod environment.
fixme_eureka_domain_test The domain name of the Eureka server in your test environment, e.g. eureka.test.myorg.com. You can safely ignore this if you don't use Eureka.
fixme_eureka_domain_prod Same as above, but for prod environment.

Example all-in-one curl command

The following curl command is an example for a project named example-riposte-project:

curl -s 'https://raw.githubusercontent.com/Nike-Inc/riposte-microservice-template/master/bootstrap_template.sh' \
| bash /dev/stdin example-riposte-project someorg \
-Dfixme_project_remotetest_url_test=https://exampleriposteproject.test.someorg.com \
-Dfixme_project_remotetest_url_prod=https://exampleriposteproject.someorg.com \
-Dfixme_eureka_domain_test=eureka.test.someorg.com \
-Dfixme_eureka_domain_prod=eureka.someorg.com

back to top

Running the server

A Riposte application is ultimately just a simple standard public static void main style java app. No container to deal with, no funky setup requirements. The main class is com.myorg.Main. The only thing you have to do when launching the app is to set the following System properties: @appId and @environment. By default this app uses Archaius-style conventions for property/environment management, and it needs those two System properties to know which src/main/resources/*.properties and/or src/main/resources/*.conf files to load (this template actually uses Typesafe Config under the hood by default, not Archaius, but the Archaius naming conventions are useful and allow you to trivially switch to Archaius if you want). @appId is the name of your project (i.e. the rootProject.name you set in settings.gradle), and @environment is the environment you're running in (i.e. local, test, or prod).

For example if your project is named foo and you're running on your local box then you'd set [email protected]=foo [email protected]=local for your System properties when starting the server (see the properties section for more information on how the Archaius-style conventions work).

There are three out-of-the-box ways to launch the app:

  1. A simple method for launching the app during development is directly in your IDE. Depending on your development style this may be the most efficient launch method for rapid iteration. For example in IntelliJ you can just right click on the com.myorg.Main class and select either Run 'Main.main()' or Debug 'Main.main()' from the right-click-menu. Selecting the debug option will let you hit breakpoints immediately without launching a remote debug session.

    NOTE: The first time it runs using this launch option it will fail, complaining about the @appId and @environment System properties. You will need to edit the configuration for this launch option to include the [email protected]=foo [email protected]=local System properties. But you only need to do this once - any later launches will remember these settings.

  2. The gradle build file is setup with the application plugin, so if you want to launch from the command line you can perform the following command: ./gradlew run. It is already configured with the proper @appId and @environment System properties for local development so it should just work.

    NOTE: If you want to do remote debugging you'll need to launch it with the --debug-jvm flag, e.g.: ./gradlew run --debug-jvm. This will cause the server to pause on startup until you connect a remote debug session on port 5005.

  3. When the gradle build runs, it creates a "shadow jar" (a.k.a. "fat jar") at [projectroot]/build/libs/[appId]-[version].jar. This shadow jar is a single executable jar file that contains the entire application, including third party libraries. The only classpath it needs is itself, so to launch the application using this shadow jar you would do something like the following:

    java -jar [email protected]=foo [email protected]=local build/libs/*.jar

    This is usually how you would want to launch a Riposte app on a production server. Note that you can add standard JVM args to this command to support remote debugging, change memory or garbage collection options, etc. There is a debugShadowJar.sh script at the root of the project that already contains this command and configures remote debugging on port 5005.

Enabling embedded Cassandra

This template microservice contains an embedded Cassandra database as an example of interacting with a database in an async nonblocking way. Unfortunately this significantly increases the startup time of the application, so it's disabled by default. When you want to experiment with the Cassandra endpoint you can enable embedded Cassandra by adding the following line to your [appname]-core-code/src/main/resources/[appname]-local-overrides.conf file: disableCassandra=false.

back to top

Example endpoints

The following example endpoints are available in this template project. By default they are available at http://localhost:8080/[path]. It's recommended that you use a REST client like Postman for making the requests so you can easily specify HTTP method, payloads, headers, etc, and fully inspect the response:

Note that in a real production application you might want to protect all endpoints except /healthcheck. For the examples above only POST /exampleBasicAuth is protected. See the comments and implementation of AppGuiceModule.basicAuthProtectedEndpoints(Set) to see how to switch to protect all endpoints except /healthcheck.

Other things to try

In addition to the example endpoints there is some core Riposte functionality to investigate:

back to top

Template Application properties and dependency injection

Application properties (Typesafe Config using Archaius conventions)

By default the template application is setup to use Typesafe Config with Archaius-style conventions for property file/environment management. If you look in [projectroot]/[appId]-core-code/src/main/resources you'll see several *.conf files. When the app server is launched it requires two System properties to be set: @appId and @environment. These System properties are used by Typesafe Config to determine which properties files to load. [appId].conf is always loaded - it acts as the default set of properties. The other properties files are named in the format [appId]-[environment].conf, so after loading the default properties file Typesafe Config will use the @environment System property to construct the correct properties filename for the environment you're currently in and load that file as an addition and override of the default properties file.

Whenever there are property name collisions with the default and environment-specific properties files, the environment-specific ones will win since they are loaded after the default properties file. You can also add new properties in the environment file that don't exist in the default file and they will be loaded into Typesafe Config as well when that environment is specified. NOTE: You can pass in System properties to the application and they will be available for use in Typesafe Config, and System properties will override file-based properties if they have the same name.

You may notice there is one properties file that doesn't follow this [appId]-[environment].conf convention: [appId]-local-overrides.conf. If you look in [appId]-local.conf you'll see the last line says: include "[appId]-local-overrides.conf". This tells Typesafe Config to load the local-overrides properties file after it loads the local properties file. Since the local-overrides properties file is in .gitignore it is ignored by git which allows you to setup any temporary custom configuration for your local box without worrying about modifying anything that is checked into git.

What if I don't want to use Typesafe Config?

The way Typesafe Config is used provides an easy and convenient way to have environment-specific properties, and we have a helper Guice module that knows how to extract the Typesafe Config properties and register them with Guice so you can inject property values into your code trivially without any setup (see the Guice section below). That said, you are not required to use Typesafe Config in your project. To replace it with something else you would need to do the following:

  1. If you want to use Archaius instead, change your Main class from extending TypesafeConfigServer to ArchaiusServer (found in the com.nike.riposte:riposte-archaius dependency). If you don't want Typesafe Config or Archaius you can write your own class that mimics what TypesafeConfigServer and ArchaiusServer do but have your own property loading strategy.
  2. By default Guice expects to find certain properties from your properties files registered with it so that they can be injected into GuiceProvidedServerConfigValues (which is used by the template application's AppServerConfig class). This is done by passing a PropertiesRegistrationGuiceModule to AppServerConfig, and by default a module that extracts the properties from Typesafe Config is used. So if you remove Typesafe Config you'll need to either:

    a. Create AppServerConfig with a different PropertiesRegistrationGuiceModule that knows about your application's properties, and make sure those properties include all the config bits expected by GuiceProvidedServerConfigValues (see [appId].conf for the recommended default values).

    b. --OR-- Modify your AppGuiceModule to expose all the necessary config bits manually as @Named injectable beans. To create this manually in one of your Guice modules you'd do something like:

        @Provides
        @Singleton
        @Named("endpoints.port")
        public int endpointsPort() { return 4242; }

    c. --OR-- A combination of the above. Again, Guice just needs to be able to inject everything marked @Inject in GuiceProvidedServerConfigValues. How you set it up is up to you, so you could have some of them provided via PropertiesRegistrationGuiceModule and others via manual bean definition.

Of course, if you also rip out Guice (see below) and start the app using your own custom implementation of ServerConfig then some of the above won't be necessary, but you're on your own at that point. Ultimately all you have to do is figure out how to pass a valid instance of ServerConfig to the Riposte server when it is created in com.myorg.Main.

Dependency injection (Guice)

By default this template application uses Guice for dependency injection. The guts of Riposte do not use dependency injection and get all the configuration and objects needed to run the server infrastructure through the ServerConfig that is passed into the server when it is created. But since the ServerConfig.appEndpoints() method provides the endpoint objects that will be registered with the server, and those endpoints are where the vast bulk of your application will reside, all you have to do is make sure your endpoints are Guice enabled and your app will effectively be fully dependency-injection-capable.

AppGuiceModule is the main Guice module for the application. It exposes the @Named("appEndpoints") bean that returns the list of endpoints for your app to use, so whenever you create a new endpoint just make sure you add it to the argument list for this method and return it in the return list for the method, and your endpoint will have full dependency injection support (by adding it to the argument list for the method Guice will auto-create an instance for you and perform all the requested injection in that class).

Other potentially interesting info regarding how this application's Guice setup is done:

What if I don't want to use Guice?

You're not required to use Guice in your application. The only strict requirement is that your application passes a valid ServerConfig into the Riposte server when it is created in com.myorg.Main. ServerConfig is just an interface, so you're free to use other dependency injection implementations if you want to wire up your endpoints, and as long as your ServerConfig returns the wired-up objects you're good to go.

And if you don't like dependency injection at all just rip it out. Again, as long as you provide a valid ServerConfig implementation to the Riposte server when it is created in com.myorg.Main it doesn't matter what technologies you do or don't use under the hood.

back to top

Building endpoints

Building endpoints is fairly straightforward. Just add classes that extend StandardEndpoint or ProxyRouterEndpoint and make sure they are returned via the AppGuiceModule.appEndpoints(...) method. The StandardEndpoint and ProxyRouterEndpoint base classes define abstract methods that you have to override, and the Riposte server uses those methods to route requests to the correct endpoints and execute the endpoint on the incoming request when appropriate.

See the Example*Endpoint classes for examples of both the standard non-blocking and proxy-style endpoints, and examples of using the error handling and validation system.

Don't forget to add new endpoint classes to AppGuiceModule.appEndpoints(...) or they will not be registered with the server and you'll get 404 errors when trying to hit them!

How to build endpoints? Blocking or non-blocking?

Even though the StandardEndpoint endpoints are inherently geared toward being used in a non-blocking style, it is possible to build endpoints in a blocking way by setting up the returned CompletableFuture to use the longRunningTaskExecutor to spin up a thread and do blocking stuff in that thread (e.g. calling a database or downstream system and waiting in the thread for the response). This is fine for a quick proof of concept app or if there's simply no other way to do it, but if at all possible you'll want to do things in a non-blocking style by using async non-blocking drivers for database calls, downstream HTTP calls, etc, that return a future.

Why? Blocking-style endpoints have the performance characteristics of traditional thread-per-request synchronous Servlet-based applications. This means every concurrent request requires a new thread that sits around until the blocking work is done, and when you have too many threads going at once you'll take a noticeable performance hit due to context switching. The longRunningTaskExecutor is (by default) set up to be an unlimited thread pool where new threads are spun up as necessary and reclaimed after 60 seconds of being idle, so at least you wouldn't need to manually fiddle with thread pool sizes, but at the same time under load it would have the general performance characteristics of a blocking Servlet-based app where the app may fall over long before CPU, memory, and other resources have been used up on the machine. By building things in non-blocking style using proper async non-blocking drivers (e.g. for database and downstream HTTP calls) so that thread counts do not increase as more concurrent requests enter the system, you'll find that the app is able to fully utilize the CPU/memory/network/etc resources of the machine before performance drops.

Of course sometimes extra threads are non-negotiable. For example if you have to do complex calculations that take a long time, those calculations have to be executed somewhere, and in these cases you will need to use the longRunningTaskExecutor to spin up threads to do the work (or use your own custom thread pool). But for anything that interfaces with another application on the machine (e.g. database calls on the local box) or another system entirely (e.g. downstream HTTP calls to another server) there is often a non-blocking driver that doesn't require extra threads.

TLDR: Waiting for downstream systems to do work? Use an async nonblocking driver to prevent extra threads. Doing serious crunching in the app server itself? You'll need to spin up an extra thread (use the longRunningTaskExecutor). See below for more info on how to identify the best way to build a given endpoint.

More on building non-blocking endpoints

The StandardEndpoint endpoints are implementation-agnostic. Their execute method returns a CompletableFuture, which just means the endpoint is saying "I'll finish what I'm doing at some point in the future and will be ready to give you the response to send to the client at that time". And since it's a CompletableFuture, Riposte simply registers a listener on that future so it gets automatically notified when the job is done and will send the response at that point. CompletableFutures are composable so you can parallelize your work.

IMPORTANT NOTE: It's up to the CompletableFuture to handle threading issues. This means you can get yourself in trouble if you're not careful. Carefully read the javadoc on the NonblockingEndpoint.execute(RequestInfo, Executor, ChannelHandlerContext) method to familiarize yourself with some of the pitfalls and best practices, but in general the rules are:

As you are composing your CompletableFutures you may find yourself stuck with similar future-type objects that aren't directly compatible. For example Google Guava's ListenableFuture. It's a very similar object, but you can't return it directly from a StandardEndpoint.execute() method call. In these cases you can transform these other structures into a CompletableFuture using a simple library. In the Guava ListenableFuture example you just need to pull in the net.javacrumbs.future-converter:future-converter-java8-guava dependency and call the FutureConverter.toCompletableFuture(ListenableFuture) method. Similar libraries are available from the same developer for converting RxJava's Observable and for converting Spring's ListenableFuture. See the developer's page for details.

Project errors and error handling

Error handling and validation is the same no matter which endpoint type you use. For errors, the template app is wired up with Backstopper via the BackstopperRiposteConfigGuiceModule Guice module registration (see AppServerConfig). It is designed to guarantee that any error will be shown to the user in the same error contract, with all errors matching up with one of the enum values in SampleCoreApiError or your application's ProjectApiError. If the error handler doesn't recognize the exception it will generate a generic 500 error for the user (mapped from SampleCoreApiError.GENERIC_SERVICE_ERROR if you want to see the code and message sent to the user). If it does recognize the exception it will intelligently convert it to one or more of the SampleCoreApiError or ProjectApiError enum values, which are in turn converted into the error contract for the user.

The main way to manually throw an error in the application so that it will be handled the way you want is to throw a com.nike.backstopper.exception.ApiException. There's a builder for this exception that lets you specify the ApiError instances you want returned to the user (ApiError is the interface that SampleCoreApiError and ProjectApiError implement), and you can also include the exception message you want, any Throwable that caused the error, and a list of extra key/value pairs that you want to be logged along with the error when it is handled. It is also possible to add some extra dynamic metadata to the error contract shown to the user by wrapping one or more of the ApiError instances with new ApiErrorWithMetadata(originalApiError, metadataMap).

The error handling system is also linked with the validation system to make translating from validation errors to the proper user-facing response as easy and invisible to the developer as possible. See the next two sections on validation for details.

NOTE: When an error is handled it is logged with as much information about the request as possible. It is also logged with a UUID that is returned to the user in both the response body and response headers, so if a customer gives you an error ID you can easily look up that particular error instance in your logs to get all the details of the request that triggered the error and what went wrong.

Incoming request content automatic validation

Your endpoints can have automatic validation done on the incoming request body before the endpoint's execute method is ever called. In order for this validation to be done the following three things must happen:

  1. Your ServerConfig.requestContentValidationService() must return a non-null instance that performs the validation. By default the app wires this up to Backstopper's JSR 303 validation system for seamless translation of validation errors to user-facing errors using SampleCoreApiError and ProjectApiError enum values as the go-between mapping. But if you want to use a different validation system you can - just have ServerConfig.requestContentValidationService() return your own custom validation service.
  2. Your endpoint must return a non-null TypeReference from its requestContentType() method. This causes Riposte to deserialize the raw incoming request body bytes to whatever object type you specify, which is in turn passed into the validation service. NOTE: This should be handled for you automatically by subclasses of StandardEndpoint<I, O> as long as you specify the <I> generics argument when defining your subclass. Under normal circumstances you do not need to override requestContentType().
  3. Your endpoint's isValidateRequestContent(RequestInfo) method must return true to tell Riposte that you want validation performed on the request body content for that endpoint. This method defaults to true so you shouldn't need to do anything with this method unless you want to turn validation off for that endpoint or if you only want to do validation sometimes depending on the RequestInfo passed in.

Since all the above steps are either one-time-setup or part of standard endpoint creation you shouldn't need to do anything under normal circumstances and you simply need to annotate your model objects with JSR 303 Bean Validation annotations, but knowing how it all fits together can be helpful if something isn't working as expected.

See the ExampleEndpoint.Post endpoint class for a concrete example showing how all this works together. To see it in action start your server and send a POST to http://localhost:8080/example with the request body:

    {
      "input_val_1": 1,
      "input_val_2": "whee",
      "throwManualError": false
    }

It should respond with a 201 and echo your request body back to you. To see the validation work, send this instead (note the missing "inputval*" fields):

    {
      "throwManualError": false
    }

You'll get back a 400 with two errors, one for each JSR 303 violation. Take a look at ExampleEndpoint.ErrorHandlingEndpointArgs and notice how each JSR 303 annotation's message is a string representing one of the ProjectApiError's enum values. This is how the error handling system knows how to map a validation violation to a proper user-facing error response.

IMPORTANT NOTE: Since the JSR 303 annotation's message field must be a string, and those strings must map to a SampleCoreApiError or ProjectApiError enum, there is the potential for typos, copy/paste errors, or other problems that prevent the JSR 303 annotation's message from lining up properly. The VerifyJsr303ContractTest class is a unit test already set up in the template application designed to prevent these errors. It trolls through your application classes looking for JSR 303 annotations and makes sure that each one's message can be successfully mapped to a SampleCoreApiError or ProjectApiError. Do not disable or delete this unit test!

Java Bean Validation (JSR 303) on arbitrary objects

You are not limited to the automatic request content validation for using the JSR 303 validation services. If you want to run the same JSR 303 validation on arbitrary objects at arbitrary times, and have any violations automatically kicked to the error handling system and handled the same way, you can do so. Simply @Inject a ClientDataValidationService or a FailFastServersideValidationService into your code and call one of the validate* methods.

ClientDataValidationService is intended for validating user-supplied data in a way that will map to a HTTP status 4xx error with details given to the user on what went wrong. This is what the automatic incoming request body validation uses.

FailFastServersideValidationService is intended for validating serverside data in a way that will map to a HTTP status 5xx error with a generic message for the user, but logged in the system with the details of the specific JSR 303 violations that occurred. You would use this (for example) to make sure you're sending downstream services valid objects and receiving valid objects back. Under normal circumstances no violations would occur, but if they do then we don't leak details about our server internals to the user while still logging all the relevant information so the developers can investigate and fix the bugs.

back to top

Metrics

Codahale Metrics are supported out of the box, and several useful Riposte server metrics are gathered including detailed throughput and latency info about each endpoint (either per-endpoint, or grouped by HTTP method, or grouped by HTTP response code), and several other metrics. You can also enable JVM metrics by setting the metrics.reportJvmMetrics property to true in your properties files or passing it in as a System Property on app launch.

There are several reporting options you can choose from to retrieve the metrics, each enabled or disabled with a property from your application properties files:

These options are not mutually exclusive. You can have multiple metrics reporters enabled at the same time, and you can add your own custom reporters - just follow the pattern in AppGuiceModule.metricsReporters(...).

Riposte contains a convenience object for tracking and reporting on custom metrics in addition to the automatically-handled server metrics. Inject CodahaleMetricsCollector into your code and use getMetricRegistry() to retrieve the Codahale MetricRegistry and register any metrics you want. CodahaleMetricsCollector also contains convenience helper methods for timers, meters, and counters - simply pass lambdas to the timed(...), metered(...), or counted(...) methods to have those lambdas measured.

back to top

Remote tests submodule

The remote-tests submodule is intended for functional tests, performance tests, and other remote tests where you're making HTTP calls against a fully independent outside environment application stack. These types of tests don't make sense to run at compiletime and are intended to run against a specified environment so they are segregated from the main application's tests found in the core-code submodule. As an example, the remote-tests submodule comes with a functional test for verifying that basic auth is working correctly (BasicAuthVerificationFunctionalTest). You execute the functional tests with the following gradle command: ./gradlew functionalTest -DremoteTestEnv=[environment]

The value of the -DremoteTestEnv=[environment] System property can be local, test, or prod, and is used to determine which of the *-functionaltest-[environment].conf properties files to load when running the tests. Running against your local environment should work properly as long as the application is running locally. To run against a deployed test or prod environment you'll need to fix the host property in the *-functionaltest-test.conf and *-functionaltest-prod.conf files for your deployed project.

back to top

Component tests

Since Riposte servers start up quickly (usually less than 1 second) and don't require a container to run, they can be launched during unit tests in order to test your application from a black-box perspective. They can be thought of as integration or end-to-end tests, but they run at compile time along with the rest of your unit tests so you know immediately when something breaks rather than waiting until you've deployed your application into a test environment and run functional tests against it. See VerifyExampleEndpointComponentTest and VerifyBasicAuthIsConfiguredCorrectlyComponentTest for examples. This is a powerful technique that can give you high confidence that major refactors did not break your application's API contracts (or functionality in general).

back to top

Removing the example code

Once you're satisfied that you understand how to build endpoints and use the error handling & validation system you're free to delete the example stuff from the template project. Simply search for TODO: EXAMPLE CLEANUP in the project and follow the instructions in each comment to remove the example stuff from that area. The main important pieces are:

There are a few other things you might want to clean up depending on your needs - again just do a search for TODO: EXAMPLE CLEANUP in the project.

back to top

Using Java 11 with this project

This project is Java 11 ready. Simply find the two references to JavaVersion.VERSION_1_8 in build.gradle and replace them with JavaVersion.VERSION_11. Then build and run with a Java 11 JDK. No other changes are needed.

License

This Riposte microservice template is released under the Apache License, Version 2.0