The GreenHopper Rapid Board was built to fulfill our most-voted customer requests, including the notorious support for multiple Jira projects. This and other features, like swimlanes and ranking performance improvements, led to a change in the underlying technology stack as well.

Old to new

Since we had access to some new technologies, the previous combination of WebWork-actions, Velocity templates and PropertySet-based storage has been replaced by a slim REST service implementation, using ActiveObjects as storage solution and Google closure templates and JavaScript for client-side rendering. This means no more convoluted action class hierarchy, fragmented velocity templates and heavy server-side markup creation.

This is what the new stack looks like:

RapidBoard technology stack

All rendering to the client

Separating the responsibilities between client and server along the lines of a defined JSON structure has some really nice effects:

  • All the UI logic is now in one place. Previously, we’ve always had to deal with translating data into markup or JavaScript on the server and then sending fragments back to the client. Having the data from the server around in JSON inside the browser makes a lot of previous workarounds obsolete, since the the scripts often have access to the data without another server roundtrip
  • The server codebase is reduced to producing JSON data, instead of dealing with producing markup. This makes integration testing a whole lot easier (actually, it makes it feasible) since you’re not testing against a view (markup) but against a REST resource that provides raw data that can easily be deserialised – and is a lot less subject to change.
  • No more dealing with webwork actions or sitemesh. The interaction flow logic, especially for highly integrated, AJAX-based pages, can be expressed in JS much more naturally.
  • And lastly, decoupling. We could take the whole backend and replace it with a completely different technology, without touching the client code. Not that we expect this to be necessary, but it gives you that warm feeling of things being where they belong.

ActiveObjects storage

The capabilities of ActiveObjects to store and read large amounts of data made the new, non-reindexing ranking system possible, which was a prerequisite for a cross-project board. But ActiveObjects is also used to store Rapid Board related data: Columns, Swimlanes, Quick Filters, etc.

Even though AO entities look a lot like domain objects, we’ve decided to hide the ActiveObjects integration behind our own layer of DAOs. Since AO follows the active record pattern, the proxies are quite heavyweight and contain references to lots of AO internals. We certainly didn’t want to cache them, and the whole business of contexts and transactions looked like a source of odd problems as well.

So the GreenHopper solution uses DAOs to access AO entities and translates them into a fairly similar looking, immutable set of domain objects. These we cache, since they’re small and have lots of read access.

RapidBoard ActiveObjects

The business logic works only on the domain objects. For example, a Quick Filter domain object looks like this:

public class QuickFilter
private final Long id;
private final String name;
private final String query;
private final String description;
private final int position;

// […] getters/setters and standard builder pattern here

Since it’s immutable (and small), we cache instances in memory and even use them in method signatures instead of Long IDs.

On the data layer, that gets mapped to and from a corresponding ActiveObjects instance:

@Preload({“NAME”, “QUERY”, “DESCRIPTION”, “POS”})
public interface QuickFilterAO extends Entity, Positionable
public String getName();
public void setName(String name);

public String getQuery();
public void setQuery(String query);

// […]

Efficient Lucene

The Rapid Board was built to deal with a lot more issues in one page than the old boards were able to display. The horizontal segmentation caused by the swimlanes makes lazy scrolling or pagination rather complex, so we went the other way and optimised the server for more efficient reading.

Let’s look at a typical rapid board:

RapidBoard JQL

Every Rapid Board assembles a JQL query from the backing saved filter’s base query and Swimlane / Quick Filter JQL snippets. This is used to load the issue data. The information on how many days an issue has been in a particular status / column in its lifetime is gathered from the Lucene issue history index.

To get all this information efficiently, we’re making heavy use of the Collector API, while limiting the amount of data that’s being loaded. We avoid using DocumentIssue and even the full DocumentHitCollector provided by Jira and rely on custom collectors, based on FieldableDocumentHitCollector, to fetch just the data we need.

Even though there are lots of collector and low-level Lucene queries being executed, this works very efficiently – hundreds of issues can be loaded and transformed in around 200ms, including their status history. Ideally, data gets read from Lucene in the collector and written directly into the JSON “template” objects, then handed off to Jersey.

A lot more Javascript

With all rendering being done on the client, the amount of code written in Javascript increases. Using closure templates greatly simplifies the markup generation and helps keeping data, controller and presentation apart. We’ve been trying to keep the Javascript simple and clean – no inheritance, no closure matryoshkas.

GH.RapidBoard.ViewController.showRapidView = function(rapidViewData) {
// update the tab selector

// tell all controllers about the new view

// […]

JavaScript third-party libraries include history.js for URL/state, flot for charting and underscore.js.

A normal Rapid Board page consists of a controller script which does AJAX calls to fetch the data and a view script that does some transformations, calls the compiled soy template for rendering, and updates the DOM.


Obviously, I had to brush over some details, but I think the key takeaways are:

  • It’s possible to write a Jira plugin with a modern Javascript UI implementation by using JSON/REST as a boundary and shifting all rendering to the client
  • Jira can read data very efficiently using Lucene. It pays off to spend some time and understand what it’s capable of
  • Writing custom REST resources is simple and testable. Keep the data raw and delegate UI-specific transformations to the client script.

GreenHopper Rapid Board – A look under the h...