Sunday, December 28, 2014

Comparing Dropwizard and Ratpack

This originally started as a reply to this email thread. As I started typing it, it's grown much longer, I'm cross posting it here.

I've been looking at Ratpack on and off for the last six months or so, I've also been working heavily with Dropwizard in the mean time.

Dropwizard like Ratpack is small. The focus in Dropwizard is JSON HTTP services with a strong focus on operations, here is a flavour of what it offers:

Authentication: Dropwizard offers basic HTTP auth and an OAuth2 client (you still need to set something else to provide bearer tokens)

Deployment: A single fat jar that can be run as a server (provided a configuration file) a means to execute database migrations, and a variety of other tasks.

Metrics: The DataSource for database connections is the one from the metrics library which means all queries are metered and trivially fired into something like graphite. Also, HTTP endpoints can be easily metered, timed, and watched for exceptions. All of this means that your application provides a lot of operational observability. HTTP Clients are also provided by the metrics library and will log any issues they had.

Logging: Logging is sane and setup uniformly across the various subsystems, so during development you can configure it to append to console, while elsewhere all logs can go to somewhere sensible such as /var/log/yourapp.log.

Database: Along with metrics, queries are executed with a comment identifying where in the code they came from, though this may be a hibernate specific feature.

Health Checks: A Dropwizard application without any configured health checks will throw a warning on startup each and every time, this encourages you to write at least minimal checks.

Banners: You can add startup ASCII art banners to your app. Say what you will, but I'd miss this if it was taken away.

Migrations: Integration with liquibase is provided, I believe flyway is available as well, by way of invoking commands on the final compiled fat jar.

Admin Port: Besides your application port there is a separate admin port containing various background tasks that one may run as part of operations, providing access to healthchecks, metrics, etc... This is awesome until you're running in heroku, we're not, but I've yet to see a reasonable solution to this as Heroku only offers one routable port. For Ratpack one could just create a branch in ones handlers at the root and require HTTP basic auth/pre-shared-key/something simple over HTTPS to get the same effect, but a default implementation would be nice.

Testing: It's nice to see lots of testing levels documented and supported.

Design Opinions:

1) Dropwizard's docs encourage you to split your project (service) into three maven modules, one is the server, one is the client, and the last is the API (POJOs representing JSON) which is your API. This way you can trivially avoid dependency cycles.

2) Dropwizard heavily favours JSON (big fan here) and uses the Jackson library to do encoding/decoding

3) The page/static asset serving portion of Dropwizard is somewhat of an afterthought though sufficient for most needs. Personally, this makes a lot of sense to me as I think most apps (often single-page/fat client) should just serve static assets. Most html/css/js development should be supported by things like Node which provide first class integration with Less, linting, and all sorts of other tooling "native" to that environment.

After using Dropwizard, shortcomings:

1) First class integration of hystrix or the like isn't there, I think rather than providing HTTP clients, it should just have a factory/builder specifying endpoint, payload, and the nature of the call (can-fail+fallback, cannot fail, etc...) and then let it handle the rest. Also layer another level of metrics atop this to be able to see the overall health of requests. There is an "out of band" module for this, but again first-class.

2) No trivial hot code reload, as builds are packaged via maven-shade, especially annoying when running multiple services simultaneously -- this could be my fault.

3) A nice to have, but something around using a Dropwizard service to wrap long running worker supervision, basically long running tasks in some thread pool, and then exposing their status/progress etc...

4) Migrations can only be run from the command line, I'm not sure if it'd be a good idea to allow a web request to trigger them, but it might be easier for systems like heroku and generally working with orchestration tools operating over the network.

5) No built-in/encouraged way to put your app into maintenance mode, this would be far easier in Ratpack as you can go early in the handler chain and check for a maintenance flag, effectively making a simple decision tree.

First blush shortcomings of Ratpack:

1) Operations emphasis (see above)

2) A persistence module with something like jOOQ

Lessons learned while working with Dropwizard:

0) Cap memory usage of the JVM, constrain your various connections, grow them as you need them, otherwise you'll be hurting. This was more a “me being new to JVM” thing and assuming/hoping more kid gloves were being employed.

1) Dependency injection is fantastic, IoC containers are the devil. Magic annotations and action at a distance are pure evil, it's so much easier to have constructor injection and plain old java code creating objects. If something is requiring you to use these evil tools there are other anti-patterns at play which are painting you into this corner. Many of our bugs, gotchas etc... have been around action at a distance and dependency injection breaking the chain of type safety, just don't do it. Ratpack seems better in this regard because it's easier to programmatically get at the lifecycle of the application (please make sure you never give this up). I can't emphasize this enough, plain old language level composability is king.

2) Separate your read and write representations as even in trivial cases they're different. Don't share code here, it's a pyrrhic victory. This simplifies a great deal, by getting rid of spurious control flow and really meshes well with java 8 and the streaming APIs. Note, I'm not advocating for full blown CQRS + event sourcing.

3) Avoid hibernate, it conflates your read and write models, and just isn't sufficiently typesafe. Java isn't haskell, but it still has a good type system that you can easily push, even if it means you have the occasional superfluous objects, just to create types.

The Bigger Picture

This next part is very much my opinion and I'm attempting to look at the bigger picture, I hope it proves useful as these are the thoughts running through my head as I currently write this. I review the code of the majority of services where I work. Over and over again, the mistakes I see would only partially be covered by a better type system, expressive language, unit tests, integration tests, or pure functions. The vast majority of truly punishing mistakes are around unsuccessfully reasoning about failure. It's a skill often not taught to the vast majority of programmers and I'm only now learning it by bumping into walls and the occasional gem from which to learn. So after a language/framework/library helps you get rid of the obvious 'spelling' errors, and the honestly trivial logic mistakes, we're left with the actual failures. The ones that are hard to test; moreover, think of.

As an example, let's say we have a monolithic app, without the distributed systems headaches. We have hundreds/thousands of unique queries across our system and the vast majority of them are without a limit clause. All it would take is one writer to bug out or a user to import some data and all readers of that data are now in for a world of hurt. I'm cheating here because even "monolithic" apps are distributed systems but if said data explosion is near your core data model, I suspect cascading failures.

Frameworks like Dropwizard are really interesting to me in that they're taking very solid lessons from things like 12 factor app (http://12factor.net/) and Designing and Deploying Internet Scale Services (http://mvdirona.com/jrh/talksandpapers/jamesrh_lisa.pdf), and distilling them. More generally, I think libraries/frameworks that create tools to support these as an "engineer checklist" would dramatically improve our chance of building robust systems.