A Step-by-Step Beginner-to-Intermediate Learning Path
How to use this guide: Read chapter by chapter. Every concept builds on the previous one. Code examples are practical and runnable. Chapters marked ๐ฒ are TODO โ topics to study next.
๐ Table of Contents
| # | Chapter | Status |
|---|---|---|
| 1 | What is Dropwizard & Why Use It? | โ |
| 2 | Project Setup & Fat JAR | โ |
| 3 | Configuration with YAML | โ |
| 4 | Building REST Resources (JAX-RS) | โ |
| 5 | Bean Validation (Hibernate Validator) | โ |
| 6 | Jackson โ JSON Serialization | โ |
| 7 | Database Access with Hibernate/JDBI | โ |
| 8 | Liquibase โ Database Migrations | โ |
| 9 | Authentication (Basic Auth & OAuth2) | โ |
| 10 | Health Checks & Metrics | โ |
| 11 | Testing in Dropwizard | โ |
| 12 | Lombok โ Boilerplate Killer | โ |
| 13 | MapStruct โ Object Mapping | โ |
| 14 | Google Guice โ Dependency Injection | โ |
| 15 | OkHttp โ HTTP Client | โ |
| 16 | Hystrix โ Circuit Breaker | โ |
| 17 | TODO: Advanced Topics | ๐ฒ |
Chapter 1: What is Dropwizard & Why Use It?
๐ค The Problem It Solves
Imagine you want to build a Java REST API. You'd need to:
- Pick a web server (Jetty, Tomcat?)
- Pick a JSON library (Jackson, Gson?)
- Pick a validation library
- Pick a database library
- Pick a metrics/monitoring library
- Make sure they all work together (version conflicts are a nightmare!)
This research alone can take weeks and paralyze a developer.
โ Dropwizard's Solution
Dropwizard is a curated collection of best-of-breed libraries glued together so they work out of the box.
| Component | Library Used |
|---|---|
| Web Server | Jetty (embedded) |
| REST Framework | Jersey (JAX-RS) |
| JSON | Jackson |
| Validation | Hibernate Validator |
| Database Migrations | Liquibase |
| Metrics & Health | Metrics library |
| Logging | Logback + SLF4J |
| Config | YAML + Jackson |
๐ซ The Fat JAR Concept
Traditional Java apps rely on an app server (Tomcat, GlassFish) being installed on the machine. The problem:
"It works on my computer" ๐ค
Dropwizard packages everything โ your code + all dependencies + the web server โ into a single JAR file.
myapp.jar โ Contains Jetty, Jersey, Jackson, your code... everything!
To run it:
java -jar myapp.jar server config.yml
That's it. No app server needed. No "works on my machine" problem.
๐๏ธ Microservices Context
Microservices = breaking a big monolithic app into small, independent services. Each service:
- Does one thing well
- Has its own database
- Communicates via REST/HTTP
- Can be deployed independently
Dropwizard is perfect for microservices because each service is a single JAR that starts up fast.
๐ Dropwizard vs Spring Boot
| Feature | Dropwizard | Spring Boot |
|---|---|---|
| Philosophy | Opinionated, curated | Flexible, extensive |
| Learning curve | Easier if you know JAX-RS | Easier if you know Spring |
| Startup speed | Fast | Moderate |
| Ecosystem size | Smaller | Huge |
| Best for | REST APIs, microservices | Anything Java |
Which to choose? If you're building a focused REST microservice, Dropwizard is excellent. For complex enterprise apps, Spring Boot has more features.
Chapter 2: Project Setup & Fat JAR
๐ ๏ธ Prerequisites
Make sure you have installed:
- Java 8+ (Dropwizard 1.x requires Java 8)
- Maven 3.x
- An IDE (IntelliJ IDEA recommended)
๐ Creating a Project with Maven Archetype
The fastest way to start:
mvn archetype:generate \
-DarchetypeGroupId=io.dropwizard.archetypes \
-DarchetypeArtifactId=java-simple \
-DarchetypeVersion=1.3.29 \
-DgroupId=com.example \
-DartifactId=my-api \
-Dversion=1.0-SNAPSHOT \
-Dname=MyApi
โ ๏ธ Important: The
-Dname=MyApiparameter is used in generated file names. Always provide it!
๐ Generated Project Structure
my-api/
โโโ pom.xml
โโโ src/
โโโ main/
โโโ java/
โ โโโ com/example/
โ โโโ MyApiApplication.java โ Main class (entry point)
โ โโโ MyApiConfiguration.java โ Config class
โโโ resources/
โโโ config.yml โ Configuration file
๐ The pom.xml โ Your Project's Heart
<project>
<groupId>com.example</groupId>
<artifactId>my-api</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<dropwizard.version>1.3.29</dropwizard.version>
</properties>
<dependencies>
<!-- Core Dropwizard -->
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- This plugin creates the Fat JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!-- This tells Java which class has main() -->
<mainClass>com.example.MyApiApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
๐ The Application Class
package com.example;
import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
public class MyApiApplication extends Application<MyApiConfiguration> {
// Java's main method โ this starts everything
public static void main(String[] args) throws Exception {
new MyApiApplication().run(args);
}
@Override
public String getName() {
return "my-api"; // Name of your app
}
@Override
public void initialize(Bootstrap<MyApiConfiguration> bootstrap) {
// Register bundles (plugins) here
}
@Override
public void run(MyApiConfiguration configuration, Environment environment) {
// Register your resources (REST endpoints) here
// This is where the app "comes alive"
}
}
๐๏ธ Building & Running
# Build the Fat JAR
mvn clean package
# Run with server command
java -jar target/my-api-1.0-SNAPSHOT.jar server config.yml
# Check if it's alive (admin port 8081)
curl http://localhost:8081/ping
# Your API is on port 8080 by default
curl http://localhost:8080/hello
๐ Built-in Commands
# Validate your config file (catches errors before running!)
java -jar my-api.jar check config.yml
# Run the server
java -jar my-api.jar server config.yml
# Database commands (when Liquibase is added)
java -jar my-api.jar db migrate config.yml
java -jar my-api.jar db status config.yml
java -jar my-api.jar db drop-all --confirm-delete-everything config.yml
๐ก Pro tip: Run
checkbeforeserverin deployment scripts to catch config errors early!
Chapter 3: Configuration with YAML
๐คท Why Configuration Matters
Hard-coding things like database URLs, passwords, and ports in code is bad practice. Configuration files let you:
- Have different settings for dev/test/production
- Change settings without recompiling
- Keep secrets out of code
๐ The config.yml File
# Server ports
server:
applicationConnectors:
- type: http
port: 8080
adminConnectors:
- type: http
port: 8081
# Logging
logging:
level: INFO
loggers:
com.example: DEBUG # More verbose logging for your package
# Custom config (your own properties)
login: admin
password: secret123
# Database (added later with Hibernate bundle)
database:
driverClass: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mydb
user: dbuser
password: dbpassword
โ The Configuration Class
Every key in config.yml maps to a field in your Configuration class:
package com.example;
import io.dropwizard.Configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class MyApiConfiguration extends Configuration {
// @NotEmpty = Hibernate Validator annotation
// If this field is empty/missing, app won't start!
@NotEmpty
private String login;
@NotEmpty
private String password;
// @JsonProperty tells Jackson the YAML key name
@JsonProperty
public String getLogin() {
return login;
}
@JsonProperty
public void setLogin(String login) {
this.login = login;
}
@JsonProperty
public String getPassword() {
return password;
}
@JsonProperty
public void setPassword(String password) {
this.password = password;
}
}
๐ Multiple Config Files (Best Practice)
# Development
java -jar my-api.jar server config-dev.yml
# Production
java -jar my-api.jar server config-prod.yml
# Testing (uses H2 in-memory database)
java -jar my-api.jar server config-test.yml
Each config file can point to different databases, use different ports, etc.
๐ Accessing Config in Your App
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Access config values
String login = config.getLogin();
String password = config.getPassword();
// Pass them to classes that need them
environment.jersey().register(new MyResource(login, password));
}
Chapter 4: Building REST Resources (JAX-RS)
๐ What is JAX-RS?
JAX-RS is a Java standard (specification) for building REST APIs using annotations. Dropwizard uses Jersey as the JAX-RS implementation.
The key idea: annotate a regular Java class with special annotations, and Jersey turns it into a REST endpoint.
๐ Core Annotations
| Annotation | Meaning |
|---|---|
@Path("/users") |
URL path this class/method handles |
@GET |
Responds to HTTP GET requests |
@POST |
Responds to HTTP POST requests |
@PUT |
Responds to HTTP PUT requests |
@DELETE |
Responds to HTTP DELETE requests |
@Produces(MediaType.APPLICATION_JSON) |
Returns JSON |
@Consumes(MediaType.APPLICATION_JSON) |
Accepts JSON input |
@PathParam("id") |
Gets value from URL: /users/42 โ id=42 |
@QueryParam("name") |
Gets value from query string: ?name=John
|
โ Your First Resource Class
package com.example.resources;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.ArrayList;
@Path("/bookmarks") // All methods in this class handle /bookmarks
@Produces(MediaType.APPLICATION_JSON) // All responses are JSON
@Consumes(MediaType.APPLICATION_JSON) // All requests accept JSON
public class BookmarkResource {
private List<Bookmark> bookmarks = new ArrayList<>();
// GET /bookmarks โ returns all bookmarks
@GET
public List<Bookmark> getAllBookmarks() {
return bookmarks;
}
// GET /bookmarks/42 โ returns one bookmark by ID
@GET
@Path("/{id}")
public Bookmark getBookmark(@PathParam("id") long id) {
return bookmarks.stream()
.filter(b -> b.getId() == id)
.findFirst()
.orElseThrow(() -> new NotFoundException("Bookmark not found: " + id));
}
// POST /bookmarks โ create a new bookmark
@POST
public Response createBookmark(Bookmark bookmark) {
bookmarks.add(bookmark);
return Response.status(Response.Status.CREATED)
.entity(bookmark)
.build();
}
// DELETE /bookmarks/42 โ delete a bookmark
@DELETE
@Path("/{id}")
public Response deleteBookmark(@PathParam("id") long id) {
bookmarks.removeIf(b -> b.getId() == id);
return Response.noContent().build(); // 204 No Content
}
// GET /bookmarks/search?tag=java โ filter by query param
@GET
@Path("/search")
public List<Bookmark> searchByTag(@QueryParam("tag") String tag) {
// filter logic here
return bookmarks;
}
}
๐ฆ The Domain Model (Bookmark.java)
package com.example.core;
public class Bookmark {
private long id;
private String url;
private String title;
private String tag;
// Default constructor (required for JSON deserialization)
public Bookmark() {}
public Bookmark(long id, String url, String title) {
this.id = id;
this.url = url;
this.title = title;
}
// Getters and setters
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getTag() { return tag; }
public void setTag(String tag) { this.tag = tag; }
}
๐ Register the Resource in Application
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Without this line, Dropwizard doesn't know about your resource!
environment.jersey().register(new BookmarkResource());
}
๐ง HTTP Status Codes Quick Reference
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT |
| 201 | Created | Successful POST (new resource created) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Not authenticated |
| 403 | Forbidden | Authenticated but no permission |
| 404 | Not Found | Resource doesn't exist |
| 500 | Server Error | Something blew up |
๐งโ๐ป Recommended Folder Structure
src/main/java/com/example/
โโโ MyApiApplication.java โ Entry point
โโโ MyApiConfiguration.java โ Config class
โโโ core/ โ Domain models / entities
โ โโโ Bookmark.java
โโโ db/ โ Database access (DAOs)
โ โโโ BookmarkDAO.java
โโโ resources/ โ REST endpoints
โ โโโ BookmarkResource.java
โโโ auth/ โ Authentication
โโโ MyAuthenticator.java
Chapter 5: Bean Validation (Hibernate Validator)
โ Why Validate?
Users send bad data. Always. Never trust input. Validation catches bad data before it hits your database.
Without validation:
{ "url": "", "title": null }
This would save garbage data to your database!
๐ฆ Dependency (already included in dropwizard-core)
Dropwizard includes Hibernate Validator automatically. No extra dependency needed!
๐ท๏ธ Common Validation Annotations
import javax.validation.constraints.*;
import org.hibernate.validator.constraints.*;
public class Bookmark {
private long id;
@NotEmpty(message = "URL cannot be empty")
@URL(message = "Must be a valid URL")
private String url;
@NotEmpty
@Size(min = 1, max = 200, message = "Title must be 1-200 characters")
private String title;
@NotNull
private String tag;
@Min(value = 0, message = "Views cannot be negative")
private int views;
@Email(message = "Must be a valid email")
private String authorEmail;
@Pattern(regexp = "^[A-Z].*", message = "Must start with uppercase")
private String category;
}
โ
Using @Valid in Resources
Add @Valid before the parameter โ Dropwizard validates automatically!
@POST
public Response createBookmark(@Valid Bookmark bookmark) {
// If bookmark fails validation, Dropwizard returns 422 Unprocessable Entity
// with details about what's wrong โ automatically!
bookmarks.add(bookmark);
return Response.status(201).entity(bookmark).build();
}
If validation fails, the client gets:
{
"errors": [
"bookmark.url may not be empty",
"bookmark.title size must be between 1 and 200"
]
}
๐ก๏ธ Validating Config Too!
public class MyApiConfiguration extends Configuration {
@NotEmpty // App won't start if login is missing in config.yml!
@JsonProperty
private String login;
@NotEmpty
@JsonProperty
private String password;
}
โ๏ธ Custom Validator
// 1. Create the annotation
@Documented
@Constraint(validatedBy = NoSpacesValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoSpaces {
String message() default "Must not contain spaces";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. Create the validator logic
public class NoSpacesValidator implements ConstraintValidator<NoSpaces, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
return value == null || !value.contains(" ");
}
}
// 3. Use it
public class Bookmark {
@NoSpaces
private String slug;
}
Chapter 6: Jackson โ JSON Serialization
๐ What is Jackson?
Jackson converts between Java objects โ JSON. This is called serialization (Java โ JSON) and deserialization (JSON โ Java).
Dropwizard uses Jackson automatically for all REST responses. When your method returns a Bookmark object, Jackson converts it to JSON for you.
๐ฆ Dependency
Already included in dropwizard-core!
๐ท๏ธ Key Jackson Annotations
import com.fasterxml.jackson.annotation.*;
public class Bookmark {
// Changes the JSON key name
@JsonProperty("bookmark_id")
private long id; // Java field "id" โ JSON key "bookmark_id"
// Include in JSON even if null
@JsonInclude(JsonInclude.Include.ALWAYS)
private String title;
// Exclude from JSON output
@JsonIgnore
private String internalNotes;
// Exclude null values from JSON
@JsonInclude(JsonInclude.Include.NON_NULL)
private String optionalField;
// Custom date format
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
// Map JSON โ object (deserialization constructor)
@JsonCreator
public Bookmark(@JsonProperty("bookmark_id") long id,
@JsonProperty("url") String url) {
this.id = id;
this.url = url;
}
}
๐บ๏ธ Working with Unknown Fields
// Ignore any extra JSON fields you don't have a field for
@JsonIgnoreProperties(ignoreUnknown = true)
public class Bookmark {
private String url;
// JSON might have "extra_field" โ this annotation ignores it instead of throwing an error
}
โ๏ธ Customizing Jackson in Dropwizard
@Override
public void initialize(Bootstrap<MyApiConfiguration> bootstrap) {
// Customize the ObjectMapper
bootstrap.getObjectMapper()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
๐ Polymorphic Types (Advanced)
// Base class
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = WebBookmark.class, name = "web"),
@JsonSubTypes.Type(value = BookBookmark.class, name = "book")
})
public abstract class Bookmark { }
public class WebBookmark extends Bookmark { private String url; }
public class BookBookmark extends Bookmark { private String isbn; }
// JSON will look like:
// { "type": "web", "url": "https://..." }
// { "type": "book", "isbn": "978-..." }
Chapter 7: Database Access with Hibernate/JDBI
๐๏ธ Your Two Options in Dropwizard
| Option | Description | Best For |
|---|---|---|
| Hibernate (JPA) | ORM โ maps Java objects to DB tables | Complex domain models |
| JDBI | Thin wrapper over JDBC โ write SQL directly | Simple, fast queries |
๐ฆ Dependencies
<!-- For Hibernate -->
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-hibernate</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<!-- MySQL Driver (use 5.x to avoid Liquibase conflicts!) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
๐๏ธ Part A: Hibernate
Step 1 โ The Entity (maps to a DB table)
package com.example.core;
import javax.persistence.*;
@Entity // This Java class = a database table
@Table(name = "bookmarks") // Table name in the database
@NamedQueries({
// Pre-defined queries attached to this class
@NamedQuery(
name = "com.example.core.Bookmark.findAll",
query = "SELECT b FROM Bookmark b" // JPQL (not SQL!)
),
@NamedQuery(
name = "com.example.core.Bookmark.findByTag",
query = "SELECT b FROM Bookmark b WHERE b.tag = :tag"
)
})
public class Bookmark {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-increment
private long id;
@Column(name = "url", nullable = false)
private String url;
@Column(name = "title")
private String title;
@Column(name = "tag")
private String tag;
// Getters, setters, default constructor...
}
Step 2 โ The DAO (Data Access Object)
package com.example.db;
import io.dropwizard.hibernate.AbstractDAO;
import com.example.core.Bookmark;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import java.util.List;
import java.util.Optional;
public class BookmarkDAO extends AbstractDAO<Bookmark> {
public BookmarkDAO(SessionFactory factory) {
super(factory); // AbstractDAO handles Hibernate session management
}
// Get all bookmarks
public List<Bookmark> findAll() {
return list(namedQuery("com.example.core.Bookmark.findAll"));
}
// Get by ID โ returns Optional (empty if not found)
public Optional<Bookmark> findById(long id) {
return Optional.ofNullable(get(id));
}
// Get by tag โ using named query parameter
public List<Bookmark> findByTag(String tag) {
Query<Bookmark> query = namedQuery("com.example.core.Bookmark.findByTag");
query.setParameter("tag", tag);
return list(query);
}
// Create or update
public Bookmark save(Bookmark bookmark) {
return persist(bookmark); // AbstractDAO's persist = INSERT or UPDATE
}
// Delete
public void delete(Bookmark bookmark) {
currentSession().delete(bookmark);
}
}
Step 3 โ Register Hibernate Bundle in Application
public class MyApiApplication extends Application<MyApiConfiguration> {
// Create the Hibernate bundle โ tell it about all your entities!
private final HibernateBundle<MyApiConfiguration> hibernate =
new HibernateBundle<MyApiConfiguration>(Bookmark.class) { // list ALL entities here
@Override
public DataSourceFactory getDataSourceFactory(MyApiConfiguration config) {
return config.getDataSourceFactory();
}
};
@Override
public void initialize(Bootstrap<MyApiConfiguration> bootstrap) {
bootstrap.addBundle(hibernate); // Register the bundle!
}
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Create DAO with Hibernate session factory
final BookmarkDAO dao = new BookmarkDAO(hibernate.getSessionFactory());
// Pass DAO to resource
environment.jersey().register(new BookmarkResource(dao));
}
}
Step 4 โ Config file addition
# config.yml
database:
driverClass: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/bookmarks_db
user: root
password: secret
properties:
charSet: UTF-8
hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect
maxWaitForConnection: 1s
validationQuery: "/* MyService Health Check */ SELECT 1"
minSize: 2
maxSize: 8
checkConnectionWhileIdle: false
Step 5 โ Add DataSourceFactory to Config class
public class MyApiConfiguration extends Configuration {
@Valid
@NotNull
private DataSourceFactory database = new DataSourceFactory();
@JsonProperty("database")
public DataSourceFactory getDataSourceFactory() { return database; }
@JsonProperty("database")
public void setDataSourceFactory(DataSourceFactory factory) {
this.database = factory;
}
}
Step 6 โ Use @UnitOfWork in Resource
public class BookmarkResource {
private final BookmarkDAO dao;
public BookmarkResource(BookmarkDAO dao) {
this.dao = dao;
}
@GET
// @UnitOfWork manages the Hibernate session + transaction for you!
// Without it, you'd have to open/close sessions manually.
@UnitOfWork
public List<Bookmark> getAllBookmarks() {
return dao.findAll();
// โ ๏ธ WARNING: Lazy-loaded fields must be accessed INSIDE this method!
}
@POST
@UnitOfWork
public Bookmark createBookmark(@Valid Bookmark bookmark) {
return dao.save(bookmark);
}
}
Chapter 8: Liquibase โ Database Migrations
๐ค The Problem Without Migrations
Imagine two developers both changing the database schema. Developer A adds a tag column, Developer B renames url to link. Who goes first? How do you track DB changes? How do you roll back?
Liquibase is like Git, but for your database schema.
๐ฆ Dependency
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>${dropwizard.version}</version>
</dependency>
โ๏ธ Register the Bundle
@Override
public void initialize(Bootstrap<MyApiConfiguration> bootstrap) {
bootstrap.addBundle(new MigrationsBundle<MyApiConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(MyApiConfiguration config) {
return config.getDataSourceFactory();
}
});
}
๐ The Migration File (migrations.xml)
Place at src/main/resources/migrations.xml:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!-- Each changeSet = one atomic database change -->
<!-- id + author combination must be globally unique! -->
<changeSet id="1" author="mitri">
<createTable tableName="bookmarks">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="url" type="VARCHAR(500)">
<constraints nullable="false"/>
</column>
<column name="title" type="VARCHAR(255)"/>
<column name="tag" type="VARCHAR(100)"/>
</createTable>
<!-- Liquibase can auto-generate rollback for createTable (just DROP TABLE) -->
</changeSet>
<changeSet id="2" author="mitri">
<addColumn tableName="bookmarks">
<column name="created_at" type="TIMESTAMP" defaultValueDate="CURRENT_TIMESTAMP"/>
</addColumn>
</changeSet>
<changeSet id="3" author="mitri">
<!-- Insert test data โ must specify manual rollback! -->
<insert tableName="bookmarks">
<column name="url" value="https://dropwizard.io"/>
<column name="title" value="Dropwizard Official Site"/>
<column name="tag" value="java"/>
</insert>
<rollback>
<!-- Tell Liquibase how to undo this changeSet -->
<delete tableName="bookmarks">
<where>url = 'https://dropwizard.io'</where>
</delete>
</rollback>
</changeSet>
</databaseChangeLog>
๐ Running Migrations
# Check current state of your database
java -jar my-api.jar db status config.yml
# Apply all pending migrations
java -jar my-api.jar db migrate config.yml
# Apply migrations up to a specific tag
java -jar my-api.jar db migrate --include-tag mytag config.yml
# Roll back last 1 change
java -jar my-api.jar db rollback --count 1 config.yml
# Danger zone! Drops everything. Only for dev!
java -jar my-api.jar db drop-all --confirm-delete-everything config.yml
๐ Liquibase vs Flyway
| Feature | Liquibase | Flyway |
|---|---|---|
| Migration format | XML, YAML, SQL, JSON | SQL (primarily) |
| Rollbacks | Built-in support | Manual in Flyway Pro |
| Author tracking | Yes (prevents conflicts) | No |
| DB independence | Yes (XML format) | SQL-only = DB specific |
| Dropwizard default | โ Yes | Via third-party bundle |
Why Liquibase wins for teams: The author field prevents naming collisions. Two developers can't accidentally create conflicting migrations.
Chapter 9: Authentication
๐ Dropwizard's Two Built-in Auth Methods
- Basic Auth โ Username + Password sent with every request
- OAuth 2.0 โ Token-based auth (used by most modern APIs)
๐ฆ Dependency
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
</dependency>
๐ค Step 1 โ The User Class
Must extend java.security.Principal in Dropwizard 1.x:
package com.example.auth;
import java.security.Principal;
public class User implements Principal {
private final String name;
public User(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
๐ Step 2 โ The Authenticator
package com.example.auth;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
import java.util.Optional;
public class MyAuthenticator implements Authenticator<BasicCredentials, User> {
private final String validLogin;
private final String validPassword;
public MyAuthenticator(String login, String password) {
this.validLogin = login;
this.validPassword = password;
}
@Override
public Optional<User> authenticate(BasicCredentials credentials)
throws AuthenticationException {
// Check if credentials match
if (validLogin.equals(credentials.getUsername()) &&
validPassword.equals(credentials.getPassword())) {
return Optional.of(new User(credentials.getUsername()));
}
// Return empty Optional = authentication failed
// Dropwizard will return 401 Unauthorized automatically
return Optional.empty();
}
}
๐ญ Step 3 โ Register in Application
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Create the authenticator with credentials from config
MyAuthenticator authenticator = new MyAuthenticator(
config.getLogin(),
config.getPassword()
);
// Register Basic Auth with the authenticator
environment.jersey().register(
new AuthDynamicFeature(
new BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(authenticator)
.setRealm("My Secure API") // Shown in browser login dialog
.buildAuthFilter()
)
);
// Enable @Auth annotation injection in resources
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
๐ Step 4 โ Protect Your Endpoints
@GET
@Path("/{id}")
@UnitOfWork
// @Auth injects the authenticated user AND protects the endpoint
public Bookmark getBookmark(@Auth User user, // user = logged in user
@PathParam("id") long id) {
// Only authenticated users reach here
return dao.findById(id)
.orElseThrow(() -> new NotFoundException("Not found: " + id));
}
๐ก Making a Request with Basic Auth
# Using curl โ -u "username:password"
curl -u "admin:secret123" http://localhost:8080/bookmarks/1
# With HTTPS (ignore self-signed cert for dev)
curl -k -u "admin:secret123" https://localhost:8443/bookmarks
# Or manually with header
curl -H "Authorization: Basic YWRtaW46c2VjcmV0MTIz" http://localhost:8080/bookmarks
Chapter 10: Health Checks & Metrics
๐ Health Checks
Health checks answer: "Is my app healthy right now?"
The admin port (8081) exposes:
-
/pingโ returns "pong" if app is up -
/healthcheckโ runs all registered health checks
Built-in Health Checks
Dropwizard provides one by default:
- DeadlockHealthCheck โ checks if JVM has thread deadlocks
When you add the Hibernate bundle, you get a free extra:
- Database connectivity check โ tries to ping the database
Writing a Custom Health Check
package com.example.health;
import com.codahale.metrics.health.HealthCheck;
public class ExternalServiceHealthCheck extends HealthCheck {
private final String serviceUrl;
public ExternalServiceHealthCheck(String serviceUrl) {
this.serviceUrl = serviceUrl;
}
@Override
protected Result check() throws Exception {
// Try to call the external service
try {
// ... make HTTP call to serviceUrl ...
boolean isReachable = pingService(serviceUrl);
if (isReachable) {
return Result.healthy("External service is UP");
} else {
return Result.unhealthy("External service is DOWN at: " + serviceUrl);
}
} catch (Exception e) {
return Result.unhealthy("Exception connecting: " + e.getMessage());
}
}
private boolean pingService(String url) {
// Simplified โ use OkHttp (Chapter 15) in real code
return true;
}
}
Registering a Health Check
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Register your custom health check
environment.healthChecks().register(
"external-service",
new ExternalServiceHealthCheck("https://api.thirdparty.com/ping")
);
}
Response When Healthy
curl http://localhost:8081/healthcheck
{
"deadlocks": { "healthy": true },
"hibernate": { "healthy": true },
"external-service": { "healthy": true, "message": "External service is UP" }
}
Response When Unhealthy (returns 500)
{
"external-service": {
"healthy": false,
"message": "External service is DOWN at: https://api.thirdparty.com/ping"
}
}
๐ Metrics
Dropwizard's Metrics library lets you measure things in your app:
import com.codahale.metrics.*;
public class BookmarkResource {
private final Meter requests; // How many requests per second
private final Timer latency; // How long requests take
private final Counter activeUsers; // Count something
private final Histogram payloadSize; // Distribution of values
public BookmarkResource(MetricRegistry metrics, BookmarkDAO dao) {
this.requests = metrics.meter("bookmark.requests");
this.latency = metrics.timer("bookmark.latency");
this.activeUsers = metrics.counter("bookmark.active-users");
}
@GET
public List<Bookmark> getAllBookmarks() {
requests.mark(); // Record a request happened
try (Timer.Context ctx = latency.time()) { // Measure how long this takes
return dao.findAll();
}
}
}
๐ Sending Metrics to Graphite
# config.yml
metrics:
reporters:
- type: graphite
host: graphite.example.com
port: 2003
prefix: myapp
frequency: 1 minute
Chapter 11: Testing in Dropwizard
๐งช Testing Philosophy
Dropwizard takes testing seriously and provides special tools that make it easy to test REST APIs without running a full server.
๐ฆ Dependency
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>${dropwizard.version}</version>
<scope>test</scope>
</dependency>
<!-- Note: JUnit is included automatically! No need to add it separately. -->
๐งฉ Level 1: Unit Testing a Resource (ResourceTestRule)
Tests just your resource class + Jersey, without a real database:
package com.example.resources;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.junit.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
import javax.ws.rs.core.Response;
import java.util.List;
public class BookmarkResourceTest {
// Mock the DAO โ no real database needed!
private static final BookmarkDAO dao = mock(BookmarkDAO.class);
// Spins up an in-memory Jersey server for testing
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
.addResource(new BookmarkResource(dao))
.build();
@Before
public void setup() {
// Define what the mock returns
when(dao.findAll()).thenReturn(List.of(new Bookmark(1L, "https://example.com", "Example")));
}
@After
public void tearDown() {
reset(dao); // Reset mock between tests
}
@Test
public void testGetAllBookmarks() {
// Make a GET request using the test client
List<Bookmark> result = resources.client()
.target("/bookmarks")
.request()
.get(new GenericType<List<Bookmark>>(){});
assertThat(result).hasSize(1);
assertThat(result.get(0).getUrl()).isEqualTo("https://example.com");
verify(dao).findAll(); // Verify DAO was called
}
@Test
public void testCreateBookmark_returns201() {
Bookmark newBookmark = new Bookmark(0, "https://new.com", "New Site");
when(dao.save(any(Bookmark.class))).thenReturn(newBookmark);
Response response = resources.client()
.target("/bookmarks")
.request()
.post(Entity.json(newBookmark));
assertThat(response.getStatus()).isEqualTo(201);
}
@Test
public void testGetBookmark_notFound_returns404() {
when(dao.findById(99L)).thenReturn(Optional.empty());
Response response = resources.client()
.target("/bookmarks/99")
.request()
.get();
assertThat(response.getStatus()).isEqualTo(404);
}
}
๐ Level 2: Integration Testing (DropwizardAppRule)
Starts the entire application โ tests everything together:
package com.example;
import io.dropwizard.testing.junit.DropwizardAppRule;
import org.junit.*;
import javax.ws.rs.client.*;
import javax.ws.rs.core.Response;
public class BookmarkIntegrationTest {
// Starts the ENTIRE app (real server, real database)
@ClassRule
public static final DropwizardAppRule<MyApiConfiguration> RULE =
new DropwizardAppRule<>(
MyApiApplication.class,
"config-test.yml" // Use test config (H2 in-memory DB)
);
private Client client;
@Before
public void setup() {
client = ClientBuilder.newClient();
}
@Test
public void testServerIsRunning() {
Response response = client
.target(String.format("http://localhost:%d/bookmarks",
RULE.getLocalPort()))
.request()
.get();
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void testAuthenticatedEndpoint() {
Response response = client
.target(String.format("https://localhost:%d/bookmarks",
RULE.getLocalPort()))
.request()
// Add Basic Auth header
.header("Authorization", "Basic " + Base64.encode("admin:secret"))
.get();
assertThat(response.getStatus()).isEqualTo(200);
}
}
๐๏ธ Testing Database Queries (DAOTest)
public class BookmarkDAOTest {
// Spins up real Hibernate with H2 in-memory database
@ClassRule
public static final DAOTestRule database = DAOTestRule.newBuilder()
.addEntityClass(Bookmark.class)
.build();
private BookmarkDAO dao;
@Before
public void setUp() throws Exception {
dao = new BookmarkDAO(database.getSessionFactory());
}
@Test
public void testSaveAndFind() {
Bookmark saved = database.inTransaction(() -> {
Bookmark b = new Bookmark();
b.setUrl("https://test.com");
b.setTitle("Test");
return dao.save(b);
});
assertThat(saved.getId()).isGreaterThan(0);
assertThat(dao.findById(saved.getId())).isPresent();
}
}
Chapter 12: Lombok โ Boilerplate Killer
๐ค The Problem
Java is verbose. For every field in a class, you write:
- A getter
- A setter
- Constructor(s)
-
equals()andhashCode() toString()
That's 30+ lines of boring code for a simple 5-field class.
โจ Lombok's Solution
Annotations that generate all that code at compile time. You write the fields. Lombok writes the rest.
๐ฆ Dependency
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope> <!-- Only needed at compile time -->
</dependency>
Also install the Lombok plugin in IntelliJ IDEA (Settings โ Plugins โ search "Lombok").
๐ท๏ธ Core Lombok Annotations
import lombok.*;
import lombok.extern.slf4j.Slf4j;
// @Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor
@Data
public class Bookmark {
private long id;
private String url;
private String title;
}
// Lombok generates: getters, setters, toString(), equals(), hashCode(), constructor
Breakdown of each annotation:
@Getter // Generates getters for all fields
@Setter // Generates setters for all fields
@ToString // Generates toString() like: Bookmark(id=1, url=https://...)
@EqualsAndHashCode // Generates equals() and hashCode() based on fields
@NoArgsConstructor // Generates: public Bookmark() {}
@AllArgsConstructor // Generates: public Bookmark(long id, String url, String title) {}
@RequiredArgsConstructor // Constructor for @NonNull and final fields only
public class Bookmark {
private long id;
private String url;
private String title;
}
๐๏ธ @builder Pattern
Immutable objects with a fluent builder API:
@Builder
@Getter // @Builder doesn't auto-generate getters
public class BookmarkRequest {
private final String url;
private final String title;
private final String tag;
}
// Usage:
BookmarkRequest request = BookmarkRequest.builder()
.url("https://example.com")
.title("Example")
.tag("java")
.build();
๐ Immutable Objects with @Value
// @Value = immutable version of @Data
// All fields are private final, only getters generated (no setters!)
@Value
public class UserId {
long id;
String username;
}
๐ Logging with @Slf4j
@Slf4j // Generates: private static final Logger log = LoggerFactory.getLogger(BookmarkResource.class);
public class BookmarkResource {
@GET
public List<Bookmark> getAll() {
log.info("Fetching all bookmarks"); // Use log directly!
log.debug("Debug details here");
log.error("Something went wrong", exception);
return dao.findAll();
}
}
โ ๏ธ Lombok + Jackson Integration
Jackson needs a default constructor for deserialization. Use both:
@Data
@NoArgsConstructor // Jackson needs this
@AllArgsConstructor // Useful for your code
@JsonIgnoreProperties(ignoreUnknown = true)
public class Bookmark {
private long id;
private String url;
private String title;
}
Or use @Builder with @Jacksonized:
@Builder
@Jacksonized // Tells Jackson to use the builder for deserialization
@Value
public class Bookmark {
long id;
String url;
String title;
}
Chapter 13: MapStruct โ Object Mapping
๐บ๏ธ The Mapping Problem
In real applications, you have different object types:
- Entity โ maps to database (has Hibernate annotations)
- DTO (Data Transfer Object) โ what you send/receive in API
- Domain Model โ internal business logic object
You need to convert between them constantly. Manually writing conversion code is error-prone and tedious.
// Manual mapping โ boring, error-prone
public BookmarkDTO toDTO(Bookmark entity) {
BookmarkDTO dto = new BookmarkDTO();
dto.setId(entity.getId());
dto.setUrl(entity.getUrl());
dto.setTitle(entity.getTitle());
// Forget one field and you have a bug!
return dto;
}
โจ MapStruct Solution
MapStruct generates these conversion methods at compile time using annotations.
๐ฆ Dependency
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
๐ง Maven Plugin (required with Lombok)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
๐ฆ Define Your Objects
// Database Entity
@Entity
@Data
public class BookmarkEntity {
@Id @GeneratedValue
private Long id;
private String url;
private String title;
private String tag;
private Date createdAt;
private String internalNotes; // Should NOT be in API response!
}
// API DTO (Data Transfer Object)
@Data
@NoArgsConstructor
public class BookmarkDTO {
private Long id;
private String url;
private String title;
private String tag;
// Notice: no internalNotes โ we don't expose this!
private String createdDate; // Different format than entity!
}
๐บ๏ธ The Mapper Interface
package com.example.mapper;
import org.mapstruct.*;
@Mapper(componentModel = "default") // Use "cdi" for CDI, "spring" for Spring
public interface BookmarkMapper {
// Simple mapping โ same field names map automatically!
BookmarkDTO toDTO(BookmarkEntity entity);
// Map from DTO back to entity
BookmarkEntity toEntity(BookmarkDTO dto);
// Custom field mapping
@Mapping(source = "createdAt", target = "createdDate",
dateFormat = "yyyy-MM-dd") // Convert Date to formatted String
@Mapping(target = "internalNotes", ignore = true) // Don't map this field
BookmarkDTO toDTOCustom(BookmarkEntity entity);
// Map a list automatically!
List<BookmarkDTO> toDTOList(List<BookmarkEntity> entities);
}
๐ญ Getting the Mapper Instance
// Approach 1: Factory method (no DI)
BookmarkMapper mapper = Mappers.getMapper(BookmarkMapper.class);
// Approach 2: With Guice (Chapter 14)
// Bind in your module, inject where needed
โ Using the Mapper in Resource
@Path("/bookmarks")
public class BookmarkResource {
private final BookmarkDAO dao;
private final BookmarkMapper mapper = Mappers.getMapper(BookmarkMapper.class);
@GET
@UnitOfWork
public List<BookmarkDTO> getAllBookmarks() {
List<BookmarkEntity> entities = dao.findAll();
return mapper.toDTOList(entities); // Entity โ DTO, one line!
}
@POST
@UnitOfWork
public BookmarkDTO createBookmark(@Valid BookmarkDTO dto) {
BookmarkEntity entity = mapper.toEntity(dto); // DTO โ Entity
BookmarkEntity saved = dao.save(entity);
return mapper.toDTO(saved); // Entity โ DTO for response
}
}
Chapter 14: Google Guice โ Dependency Injection
๐ค The Problem
As your app grows, you end up with this mess in your run() method:
public void run(Config config, Environment env) {
HibernateBundle hib = hibernate;
BookmarkDAO dao = new BookmarkDAO(hib.getSessionFactory());
BookmarkMapper mapper = Mappers.getMapper(BookmarkMapper.class);
MyAuthenticator auth = new MyAuthenticator(config.getLogin(), config.getPassword());
BookmarkResource resource = new BookmarkResource(dao, mapper, auth);
env.jersey().register(resource);
// What if BookmarkResource needs 5 more dependencies?!
}
Every class manually creates its dependencies. Change one thing โ change everywhere.
โจ Guice Solution
Inversion of Control: Instead of classes creating their dependencies, Guice creates them and injects them where needed.
๐ฆ Dependency
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.1.0</version>
</dependency>
<!-- Dropwizard + Guice integration -->
<dependency>
<groupId>ru.vyarus</groupId>
<artifactId>dropwizard-guicey</artifactId>
<version>5.7.1</version>
</dependency>
๐ Step 1 โ Mark Dependencies with @Inject
// BookmarkDAO depends on SessionFactory
public class BookmarkDAO extends AbstractDAO<BookmarkEntity> {
@Inject // Guice will provide SessionFactory
public BookmarkDAO(SessionFactory factory) {
super(factory);
}
}
// BookmarkResource depends on DAO and Mapper
public class BookmarkResource {
private final BookmarkDAO dao;
private final BookmarkMapper mapper;
@Inject // Guice will inject both!
public BookmarkResource(BookmarkDAO dao, BookmarkMapper mapper) {
this.dao = dao;
this.mapper = mapper;
}
}
๐ Step 2 โ The Guice Module (Wiring Configuration)
package com.example;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
public class MyModule extends AbstractModule {
@Override
protected void configure() {
// Bind interface โ implementation
// "Whenever someone asks for UserService, give them UserServiceImpl"
bind(UserService.class).to(UserServiceImpl.class).in(Singleton.class);
// Bind a class directly
bind(BookmarkDAO.class).in(Singleton.class);
// Bind mapper
bind(BookmarkMapper.class).toInstance(Mappers.getMapper(BookmarkMapper.class));
}
// @Provides methods create instances that need configuration
@Provides
@Singleton
public BookmarkDAO provideBookmarkDAO(SessionFactory factory) {
return new BookmarkDAO(factory);
}
}
๐ญ Step 3 โ Register with Dropwizard
Using dropwizard-guicey:
public class MyApiApplication extends GuiceApplication<MyApiConfiguration> {
@Override
public void initialize(Bootstrap<MyApiConfiguration> bootstrap) {
super.initialize(bootstrap);
bootstrap.addBundle(hibernate);
}
@Override
public void run(MyApiConfiguration config, Environment environment) {
// Resources are auto-discovered and injected!
}
@Override
protected Injector createInjector(MyApiConfiguration config) {
return Guice.createInjector(new MyModule());
}
}
๐ท๏ธ Key Guice Annotations
@Inject // Mark constructor/field/method for injection
@Singleton // One instance for entire app lifetime
@Named("dbUrl") // Disambiguate when multiple bindings of same type exist
// Usage:
@Inject @Named("dbUrl") String databaseUrl;
๐ Guice Scopes
bind(BookmarkDAO.class).in(Singleton.class); // One instance per app
bind(BookmarkDAO.class).in(RequestScoped.class); // New instance per HTTP request
bind(BookmarkDAO.class); // Default: new instance every time
Chapter 15: OkHttp โ HTTP Client
๐ Why OkHttp?
Microservices talk to each other over HTTP. You need an HTTP client to:
- Call third-party APIs (payment, email, maps)
- Call other microservices in your system
OkHttp is the most popular Java HTTP client โ fast, simple, feature-rich.
๐ฆ Dependency
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
๐ Basic GET Request
import okhttp3.*;
import java.io.IOException;
public class ExternalApiClient {
// IMPORTANT: Reuse the same OkHttpClient โ it manages connection pooling!
private final OkHttpClient client = new OkHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
public String fetchData(String url) throws IOException {
// Build the request
Request request = new Request.Builder()
.url(url)
.header("Accept", "application/json")
.header("Authorization", "Bearer my-token")
.build();
// Execute synchronously
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected response: " + response);
}
return response.body().string();
}
}
}
๐ค POST Request with JSON Body
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
public Bookmark createRemoteBookmark(BookmarkDTO dto) throws IOException {
String json = objectMapper.writeValueAsString(dto);
Request request = new Request.Builder()
.url("https://api.other-service.com/bookmarks")
.post(RequestBody.create(json, JSON)) // POST with JSON body
.build();
try (Response response = client.newCall(request).execute()) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, Bookmark.class);
}
}
โฑ๏ธ Timeouts & Configuration
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // How long to wait to connect
.readTimeout(30, TimeUnit.SECONDS) // How long to wait for data
.writeTimeout(15, TimeUnit.SECONDS) // How long to wait for upload
.retryOnConnectionFailure(true) // Retry on network errors
.build();
๐ Async Requests
public void fetchDataAsync(String url, Callback callback) {
Request request = new Request.Builder().url(url).build();
// Non-blocking โ callback is called when done
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
log.error("Request failed", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
try (response) {
String body = response.body().string();
// Process response
}
}
});
}
๐ OkHttp Interceptors (Middleware)
Interceptors run for every request โ perfect for logging, auth headers, etc.:
// Logging interceptor
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request request = chain.request();
log.info("โ {} {}", request.method(), request.url());
long start = System.currentTimeMillis();
Response response = chain.proceed(request);
long duration = System.currentTimeMillis() - start;
log.info("โ {} {} ({}ms)", response.code(), request.url(), duration);
return response;
})
.addInterceptor(chain -> {
// Auth interceptor โ add token to every request
Request authenticated = chain.request().newBuilder()
.header("Authorization", "Bearer " + getToken())
.build();
return chain.proceed(authenticated);
})
.build();
Chapter 16: Hystrix โ Circuit Breaker
โก The Cascading Failure Problem
Service A calls Service B. Service B calls Service C. Service C goes down.
Without circuit breakers:
- Service C times out after 30 seconds
- Service B waits 30s, then times out
- Service A waits 30s too = everything hangs for 30+ seconds!
- Threads pile up, memory fills up, your whole system crashes
๐ The Circuit Breaker Pattern
Like an electrical circuit breaker โ when failures exceed a threshold, it "trips" and subsequent calls fail immediately instead of waiting.
States:
- CLOSED โ Normal operation, calls pass through
- OPEN โ Too many failures, all calls fail immediately
- HALF-OPEN โ Testing if service recovered; allows one call through
๐ฆ Dependency
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.18</version>
</dependency>
โ Wrapping Calls with HystrixCommand
package com.example.hystrix;
import com.netflix.hystrix.*;
import java.io.IOException;
public class FetchBookmarkCommand extends HystrixCommand<String> {
private final String bookmarkId;
private final ExternalApiClient apiClient;
public FetchBookmarkCommand(String bookmarkId, ExternalApiClient apiClient) {
super(HystrixCommandGroupKey.Factory.asKey("BookmarkService"));
this.bookmarkId = bookmarkId;
this.apiClient = apiClient;
}
@Override
protected String run() throws Exception {
// The actual (potentially failing) call
return apiClient.fetchData("https://api.service.com/bookmarks/" + bookmarkId);
}
@Override
protected String getFallback() {
// This runs when:
// 1. run() throws an exception
// 2. Circuit is OPEN (previous failures)
// 3. Timeout occurs
return "{\"id\":\"" + bookmarkId + "\",\"status\":\"unavailable\"}";
}
}
๐ฏ Using the Command
// Synchronous execution
String result = new FetchBookmarkCommand(bookmarkId, client).execute();
// Asynchronous (returns Future)
Future<String> future = new FetchBookmarkCommand(bookmarkId, client).queue();
// Reactive (returns Observable)
Observable<String> observable = new FetchBookmarkCommand(bookmarkId, client).observe();
โ๏ธ Configuring Hystrix
HystrixCommand.Setter config = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BookmarkService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("FetchBookmark"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(3000) // 3 second timeout
.withCircuitBreakerRequestVolumeThreshold(10) // Min requests before circuit can trip
.withCircuitBreakerErrorThresholdPercentage(50) // Trip if 50% fail
.withCircuitBreakerSleepWindowInMilliseconds(5000) // Wait 5s before trying again
);
// Use in command:
public FetchBookmarkCommand(String id, ExternalApiClient client) {
super(config); // Pass config to super
this.bookmarkId = id;
this.apiClient = client;
}
๐๏ธ HystrixCommand + OkHttp (Combined Example)
@Slf4j
public class ExternalServiceClient {
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
@Inject
public ExternalServiceClient(OkHttpClient httpClient, ObjectMapper objectMapper) {
this.httpClient = httpClient;
this.objectMapper = objectMapper;
}
public BookmarkDTO getBookmark(String id) {
// Wrap the OkHttp call in Hystrix
return new HystrixCommand<BookmarkDTO>(
HystrixCommandGroupKey.Factory.asKey("ExternalBookmarks")) {
@Override
protected BookmarkDTO run() throws IOException {
Request request = new Request.Builder()
.url("https://external.api.com/bookmarks/" + id)
.build();
try (Response response = httpClient.newCall(request).execute()) {
return objectMapper.readValue(response.body().string(), BookmarkDTO.class);
}
}
@Override
protected BookmarkDTO getFallback() {
log.warn("Fallback triggered for bookmark: {}", id);
BookmarkDTO fallback = new BookmarkDTO();
fallback.setTitle("Unavailable");
return fallback;
}
}.execute();
}
}
Chapter 17: ๐ฒ TODO โ Advanced Topics to Study Next
These topics weren't covered in this guide but are important for production-ready applications:
๐ Security
- [ ] OAuth 2.0 โ Token-based authentication (JWT tokens)
- [ ] HTTPS/TLS โ Configure SSL in Dropwizard config.yml
- [ ] LDAP Authentication โ Enterprise authentication via directory services
- [ ] CORS โ Cross-Origin Resource Sharing configuration
๐ Observability
- [ ] Distributed Tracing โ Zipkin/Jaeger for tracing requests across microservices
- [ ] Prometheus โ Modern metrics collection (alternative to Graphite)
- [ ] Grafana Dashboards โ Visualizing your metrics
- [ ] Structured Logging โ JSON logs for log aggregation (ELK Stack)
- [ ] OpenTelemetry โ Unified observability standard
๐๏ธ Architecture
- [ ] Authorization โ
@RolesAllowed,@PermitAllannotations - [ ] Rate Limiting โ Prevent API abuse
- [ ] API Versioning โ Strategies for versioning your REST API
- [ ] HATEOAS โ Hypermedia-driven REST (links in responses)
- [ ] GraphQL โ Alternative to REST for flexible queries
- [ ] OpenAPI/Swagger โ Auto-generate API documentation
๐๏ธ Database
- [ ] JDBI โ Lighter-weight alternative to Hibernate
- [ ] Connection Pooling โ HikariCP configuration
- [ ] Read Replicas โ Routing reads vs writes
- [ ] Database Sharding โ Horizontal scaling
๐ณ Deployment
- [ ] Docker โ Containerizing your Fat JAR
- [ ] Kubernetes โ Orchestrating containers
- [ ] CI/CD โ GitHub Actions / Jenkins pipelines
- [ ] Blue/Green Deployment โ Zero-downtime deployments
- [ ] Service Discovery โ Consul, Eureka
โก Performance
- [ ] Async Resources โ
@SuspendedandAsyncResponsein JAX-RS - [ ] Caching โ Redis/Memcached with Dropwizard
- [ ] Connection Timeout Tuning โ Jetty thread pool settings
- [ ] GraalVM Native Image โ Compile to native binary for faster startup
๐งช Advanced Testing
- [ ] Consumer-Driven Contract Testing โ Pact framework
- [ ] WireMock โ Mock external HTTP services in tests
- [ ] TestContainers โ Real databases in Docker for integration tests
- [ ] Chaos Engineering โ Testing failure scenarios (Chaos Monkey)
๐จ Messaging
- [ ] Apache Kafka โ Event streaming
- [ ] RabbitMQ โ Message queuing
- [ ] Dropwizard Kafka Bundle โ Integration
Top comments (0)