Skip to content


Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

Modern Java Web Development

What Is Modern Web Development?

  • Good Developer Experience

    • Fast feedback loop

    • Good testability

    • JustWorks ™

  • No more WAR!

  • 12 Factor App


Java 8

  • Java 6 EOL 2013-02

  • Java 7 EOL 2015-02

  • Java 9 2016 Q4 (2017 late Q1? Jigsaw extension)

  • λ → {}

  • Interface Upgrade

    • Default and static methods

  • Stream API


  • Gradle wrapper - batteries included (gradlew)

  • Consistent, reproducible builds

    • Avoid "Works On My Machine" syndrome

  • Great IDE integration

  • Continuous build mode (TDD)

    • ./gradlew -t or ./gradlew --continuous

Gradle bootstrap

gradle init gives you…​

gradlew and gradlew.bat

Gradle wrapper scripts


Main build file, specifies plugins, tasks, dependencies


Extra project metadata settings

Indispensable plugins
  1. application

    1. Create ops friendly distributions

    2. Don’t forget to set mainClassName

  2. Gradle Shadow Plugin

    1. Creates "fat jars", plays nicely with application plugin

  3. idea

    1. Customize IntelliJ project/module/workspace

    2. Don’t VCS IDE settings

New plugins method (Incubating feature)
plugins {
  id 'java'
  id 'idea'
  id 'com.github.johnrengelman.shadow' version '1.2.2'
  // id "$id" version "$version"
Customize IntelliJ Integration
idea {
  project {
    jdkName = '1.8' // (1)
    languageLevel = '1.8' // (2)
    vcs = 'Git' // (3)
  1. Set JDK to 1.8

  2. Set target language level to 1.8

  3. Set VCS manager to Git

Producing artifacts

./gradlew shadowJar

Produces executable jar with flattened dependencies.

./gradlew installShadowApp

Produces executable jar and shell scripts for starting jar. Can also produce zip file via ./gradlew distShadowZip or tar via ./gradlew distShadowTar


What is EditorConfig?

EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs.

Don’t argue about formatting, pick a standard and stick to it.

Sample .editorconfig
root = true

[*] # for all files
indent_style = space
indent_size  = 2

# We recommend you to keep these unchanged
end_of_line              = lf
charset                  = utf-8
trim_trailing_whitespace = true
insert_final_newline     = true

Supported by many IDEs, e.g. IntelliJ CTRL+ALT+L


  • Nice functionality around LXC

  • Images, file-system layer snap-shotting

  • Lighter than Virtualization

    • Total size

    • Boot Time

  • Counters "Works On My Machine" syndrome

  • Nice way to bring up services/dependencies/mechanisms that may not be available for your OS


  • Tool to provision Docker ready VM for Mac/Win

Once setup, you need to inform your environment about the VM.

Ask docker-machine about default’s environment config
$ docker-machine env default
export DOCKER_HOST="tcp://"
export DOCKER_CERT_PATH="C:\Users\danny\.docker\machine\machines\default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval "$(C:\Program Files\Docker Toolbox\docker-machine.exe env default)"

Dockerized Redis

FROM ubuntu:14.04 # (1)
RUN apt-get update && apt-get install -y redis-server # (2)
EXPOSE 6379 # (3)
ENTRYPOINT ["/usr/bin/redis-server"] # (4)
  1. Base image from Ubuntu Trusty image

  2. Install Redis into new image

  3. Declare that container is listening on port 6379

  4. Start Redis server when container starts

$ docker build -t danhyun/redis .
$ docker run --name redis -d -p 6379:6379 danhyun/redis
$ docker exec -it redis bash

root@d42014247c2e:/# redis-cli> set hello world
OK> get hello
"world"> del hello
(integer) 1> get hello

Dockerized Postgres

Create new PostgreSQL container from existing Dockerfile
$ docker run --name postgres -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres

This command pulls down a postgres Docker image from Docker Hub, names the container postgres, detaches from session, maps container’s port 5432 to local port 5432.

Access PostgreSQL from running container’s command line
$ docker exec -it postgres bash
root@b1db931a37a7:/# psql -U postgres
psql (9.4.5)
Type "help" for help.

postgres=# \l
postgres=# create database modern;
postgres=# \c modern
You are now connected to database "modern" as user "postgres".

modern=# create table meeting (
  id serial primary key,
  organizer varchar(255),
  topic varchar(255),
  description text


modern=#insert into meeting
  (organizer, topic, description)
  ('Dan H', 'Modern Java Web Development', 'A survey of essential tools/frameworks/techniques for the modern Java developer');


modern=# select * from meeting;
 id | organizer |            topic            |                                  description
  1 | Dan H     | Modern Java Web Development | A survey of essentia tools/frameworks/techniques for the modern Java developer
(1 row)


  • Free signup

  • Rapid prototyping (free versions of services available)

Install Heroku Toolbelt

Get the Heroku toolbelt here

Prepare app for Heroku

Heroku only needs 2 things:

  1. Procfile - tells Heroku what to execute

  2. A stage task from Gradle


  • JDK 8+

    • just jar files, no binaries to install, no codegen

  • Minimal framework overhead (low resource usage, save $$$)

  • Unopinionated - Make your app solve your problems, don’t let framework get in the way

  • Reactive, Non-blocking and fully asynchronous

  • Excellent testing support


  • Functional interface

  • void handle(Context context) {}

  • send response now or delegate to the next handler


  • convenience API for specifying request handling flow

  • "if-else" for handlers

  • Chains are composable


  • Map like lookup for services

  • Immutable

  • Way to communicate between handlers


  • Promises

  • Operations

  • Blocking


Blazing fast JDBC library.


Configure HikariCP to use our dockerized PostgreSQL instance.

  dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
  username: postgres
  password: password
    databaseName: modern
    portNumber: 5432

Apply Config

Configure Hikari DataSource provider
.module(HikariModule.class, config -> {
  config.addDataSourceProperty("databaseName", "modern");
  config.addDataSourceProperty("serverName", "");
  config.addDataSourceProperty("portNumber", "5432");
Use a Config Object
.bindInstance(HikariConfig.class, configData.get("/db", HikariConfig.class))
Even better
ServerConfig configData = ServerConfig.builder()
      .require("/db", HikariConfig.class)


buildscript {
  repositories {
  dependencies {
    classpath 'org.postgresql:postgresql:9.4-1206-jdbc42'
    classpath 'org.jooq:jooq-codegen:3.7.1'
    classpath 'org.jyaml:jyaml:1.3'

dependencies {
  runtime 'org.postgresql:postgresql:9.4-1206-jdbc42'
  compile 'org.jooq:jooq:3.7.1'

  compile ratpack.dependency('hikari')

import org.jooq.util.jaxb.*
import org.jooq.util.*
import org.ho.yaml.Yaml

task jooqCodegen {
  doLast {
    def config = Yaml.load(file('src/ratpack/postgres.yaml')).db
    def dsProps = config.dataSourceProperties

    Configuration configuration = new Configuration()
      .withJdbc(new Jdbc()
      .withGenerator(new Generator()
//        .withGenerate(new Generate()
//          .withImmutablePojos(true) // (1)
//          .withDaos(true)           // (2)
//          .withFluentSetters(true)) // (3)
        .withDatabase(new Database()
      .withTarget(new Target()

  1. Generates immutable POJOs

  2. Generates DAOs

  3. Generates fluent setters for generated Records/POJOs/Interfaces

Hikari and jOOQ

DSLContext provides type-safe fluent API style querying. jOOQ will responsibly borrow and release connections from the provided DataSource.
public class DefaultMeetingRepository implements MeetingRepository {
  private final DSLContext context;

  public DefaultMeetingRepository(DSLContext context) {
    this.context = context;

  public Promise<List<Meeting>> getMeetings() {
    return Blocking.get(() ->
        .select().from(MEETING).fetchInto(Meeting.class) // (1)

  public Operation addMeeting(Meeting meeting) {
    return Blocking.op(() -> context.newRecord(MEETING, meeting).store());
  1. fetchInto(Class) provides SQL to POJO mapping. POJOs can be generated by jOOQ if desired.
public class JooqModule extends AbstractModule {
  protected void configure() {

  public DSLContext dslContext(DataSource dataSource) {
    return DSL.using(new DefaultConfiguration().derive(dataSource));


dependencies {
  compile 'biz.paluch.redis:lettuce:4.0.1.Final'
  port: 6379
public class RedisConfig {
  private String url;

  public String getUrl() {
    return url;

  public void setUrl(String url) {
    this.url = url;
RatpackServer.start(ratpackServerSpec -> ratpackServerSpec
      .serverConfig(config -> config
        .require("/db", HikariConfig.class)
        .require("/redis", RedisConfig.class) // (1)
  1. Add RedisConfig to the Registry
public class RedisModule extends AbstractModule {
  protected void configure() { }

  public RedisClient redisClient(RedisConfig config) { // (1)
    return RedisClient.create(config.getUrl());

  public StatefulRedisConnection<String, String> asyncCommands(RedisClient client) {
    return client.connect();

  public RedisAsyncCommands<String, String> asyncCommands(StatefulRedisConnection<String, String> connection) {
    return connection.async();

  public Service redisCleanup(RedisClient client, StatefulRedisConnection<String, String> connection) {
    return new Service() { // (2)
      public void onStop(StopEvent event) throws Exception {
        connection.close(); // (3)
        client.shutdown(); // (3)
  1. Get RedisConfig from Registry

  2. Service provides an opportunity to hook into Ratpack’s start/stop lifecycle events

  3. Cleanup Redis connection and client
public interface RatingRepository {
  Promise<Map<String, String>> getRatings(Long meetingId);

  default Promise<Double> getAverageRating(Long meetingId) {
    return getRatings(meetingId)
      .map(m -> m.entrySet()
        .map(e -> Pair.of(Integer.valueOf(e.getKey()), Integer.valueOf(e.getValue())))
        .flatMapToInt(pair -> IntStream.range(0, pair.right).map(i -> pair.left))

  Operation rateMeeting(String meetingId, String rating);
public class DefaultRatingRepository implements RatingRepository {
  private final RedisAsyncCommands<String, String> commands;

  public DefaultRatingRepository(RedisAsyncCommands<String, String> commands) {
    this.commands = commands;

  Function<Long, String> getKeyForMeeting = (id) -> "meeting:" + id + ":rating";

  public Promise<Map<String, String>> getRatings(Long meetingId) {
    return Promise.of(downstream ->
        .hgetall(getKeyForMeeting.apply(meetingId)) // (1)
        .thenAccept(downstream::success) // (2)

  public Operation rateMeeting(String meetingId, String rating) {
    return Promise.of(downstream ->
        String.valueOf(rating), 1
  1. Equivalent of HGETALL meeting:$id:rating

  2. Signal to downstream consumer that Lettuce is done with async activity

Composing data from Postgres and Redis
public interface MeetingService {
  Promise<List<Meeting>> getMeetings();
  Operation addMeeting(Meeting meeting);
  Operation rateMeeting(String id, String rating);
public class DefaultMeetingService implements MeetingService {

  private final MeetingRepository meetingRepository;
  private final RatingRepository ratingRepository;

  public DefaultMeetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) {
    this.meetingRepository = meetingRepository;
    this.ratingRepository = ratingRepository;

  public Promise<List<Meeting>> getMeetings() {
    return meetingRepository.getMeetings()
      .flatMap(meetings ->

          .peek(meeting ->
              .then(meeting::setRating) // (1)

  public Operation addMeeting(Meeting meeting) {
    return meetingRepository.addMeeting(meeting);

  public Operation rateMeeting(String id, String rating) {
    return ratingRepository.rateMeeting(id, rating);
  1. This is naughty, don’t perform side effects

Create a new module to register our RatingRepository and MeetingService

public class MeetingModule extends AbstractModule {
  protected void configure() {

  public RatingRepository ratingRepository(RedisAsyncCommands<String, String> commands) {
    return new DefaultRatingRepository(commands);

  public MeetingService meetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) {
    return new DefaultMeetingService(meetingRepository, ratingRepository);
public class App {
  public static void main(String[] args) throws Exception {
    RatpackServer.start(serverSpec -> serverSpec
      .registry(Guice.registry(bindings -> bindings
        .module(MeetingModule.class) // (1)
  1. Register our new module with the app

Deploying to Heroku

Main command to execute:

web: env DATABASE_URL=$DATABASE_URL build/installShadow/modern-java-web/bin/modern-java-web redis.url=$REDIS_URL

Ratpack can pick up config information from just about anywhere. Here we expose DATABASE_URL as an env variable and pass in REDIS_URL as redis.url as a program arg.

Gradle staging task
task stage(dependsOn: installShadowApp)

Create the Heroku app

$ heroku create
Creating gentle-beyond-5974... done, stack is cedar-14 // (1) |
.git // (3)
Git remote heroku added // (2)
heroku-cli: Updating... done.
  1. cedar-14 is the Java 8 platform, Heroku’s default Java offering

  2. Generated app name gentle-beyond-5974

  3. Added git remote named heroku

Heroku Redis

  1. Install Plugin heroku plugins:install heroku-redis

  2. Add to app heroku addons:create heroku-redis:hobby-dev

  3. Pass $REDIS_URL to your app

  4. Heroku redis-cli heroku redis:cli

Heroku Postgresql

  1. Add PostgreSQL to app heroku addons:create heroku-postgresql:hobby-dev

  2. Wait to come online heroku pg:wait

  3. Pass $DATABASE_URL to app

  4. Connect to remote heroku pg:psql (Requires psql installed locally)

Parsing Heroku’s Postgres URL

Heroku exposes the Postgres URL in a format that JDBC cannot parse.
public interface HerokuUtils {
  Function<String, List<String>> extractDbProperties = (url) -> {
    if (Strings.isNullOrEmpty(url)) return Collections.<String>emptyList();

    Pattern herokuDbPattern = Pattern
      .compile("postgres://(?<username>[^:]+):(?<password>[^:]+)@(?<serverName>[^:]+):(?<portNumber>[0-9]+)/(?<databaseName>.+)"); // (1)

    Matcher matcher = herokuDbPattern.matcher(url);
    if (!matcher.matches()) return Collections.<String>emptyList();

    return Stream
      .of("username", "password", "databaseName", "serverName", "portNumber")
      .map(prop -> Pair.of(prop, // (2)
      .map(pair -> pair.left.equals(pair.left.toLowerCase()) ?
          pair : Pair.of("dataSourceProperties." + pair.left, pair.right)
      .map(pair -> Pair.of("db." + pair.left, pair.right))
      .map(pair -> pair.left + "=" + pair.right)
  1. As of Java 7 you can provide group names in regex

  2. We ask for match by group name and construct a Pair of property to extracted value
public class App {
  public static void main(String[] args) throws Exception {
    List<String> programArgs = Lists.newArrayList(args);
        .apply(System.getenv("DATABASE_URL")) // (1)

    RatpackServer.start(serverSpec -> serverSpec
      .serverConfig(config -> config
        .args([]::new)) //(2)
        .require("/db", HikariConfig.class)
        .require("/redis", RedisConfig.class)
      .registry(/* registry */)
      .handlers(/* handlers */)
  1. Extract db properties if present

  2. Pass newly constructed list to Ratpack’s server config

Push to Heroku

$ git push heroku master
remote:        BUILD SUCCESSFUL
remote:        Total time: 40.614 secs
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote: -----> Compressing... done, 71.3MB
remote: -----> Launching... done, v4
remote: deployed to Heroku
remote: Verifying deploy... done.

Heroku will see that this is a Gradle project and invoke ./gradlew stage.

After the build Heroku will run the command from Procfile.

$ heroku open

Opens your newly minted webapp in your browser.


Survey of essential tools/frameworks for the modern Java developer






No releases published


No packages published