killbill-memoizeit

Changes

pom.xml 16(+16 -0)

usage/pom.xml 110(+110 -0)

Details

diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
index 83f701d..4e1b199 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
@@ -156,6 +156,8 @@ public class BusinessSubscriptionTransitionRecorder {
                 return subscriptionCreated(event);
             case RE_CREATE:
                 return subscriptionRecreated(event);
+            case TRANSFER:
+                return subscriptionTransfered(event);
             case CANCEL:
                 return subscriptionCancelled(event);
             case CHANGE:
@@ -180,6 +182,11 @@ public class BusinessSubscriptionTransitionRecorder {
         return BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
     }
 
+    private BusinessSubscriptionEvent subscriptionTransfered(final SubscriptionEvent transfered) throws AccountApiException, EntitlementUserApiException {
+        return BusinessSubscriptionEvent.subscriptionTransfered(transfered.getNextPlan(), catalogService.getFullCatalog(), transfered.getEffectiveTransitionTime(), transfered.getSubscriptionStartDate());
+    }
+
+
     private BusinessSubscriptionEvent subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
         // cancelled.getNextPlan() is null here - need to look at the previous one to create the correct event name
         return BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
@@ -212,7 +219,8 @@ public class BusinessSubscriptionTransitionRecorder {
                                                                     final ArrayList<BusinessSubscriptionTransition> transitions,
                                                                     final Currency currency) {
         if (BusinessSubscriptionEvent.EventType.ADD.equals(businessEvent.getEventType()) ||
-                BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType())) {
+                BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType()) ||
+                BusinessSubscriptionEvent.EventType.TRANSFER.equals(businessEvent.getEventType())) {
             return null;
         }
 
diff --git a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
index e21f648..b529559 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/model/BusinessSubscriptionEvent.java
@@ -40,6 +40,7 @@ public class BusinessSubscriptionEvent {
         ADD,
         CANCEL,
         RE_ADD,
+        TRANSFER,
         CHANGE,
         SYSTEM_CANCEL,
         SYSTEM_CHANGE
@@ -96,6 +97,10 @@ public class BusinessSubscriptionEvent {
         return eventFromType(EventType.RE_ADD, plan, catalog, eventTime, subscriptionCreationDate);
     }
 
+    public static BusinessSubscriptionEvent subscriptionTransfered(final String plan, final Catalog catalog, final DateTime eventTime, final DateTime subscriptionCreationDate) {
+        return eventFromType(EventType.TRANSFER, plan, catalog, eventTime, subscriptionCreationDate);
+    }
+
     public static BusinessSubscriptionEvent subscriptionPhaseChanged(final String plan, final SubscriptionState state, final Catalog catalog, final DateTime eventTime, final DateTime subscriptionCreationDate) {
         if (state != null && state.equals(SubscriptionState.CANCELLED)) {
             return eventFromType(EventType.SYSTEM_CANCEL, plan, catalog, eventTime, subscriptionCreationDate);
diff --git a/api/src/main/java/com/ning/billing/config/UsageConfig.java b/api/src/main/java/com/ning/billing/config/UsageConfig.java
new file mode 100644
index 0000000..d4cf6bd
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/config/UsageConfig.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.config;
+
+import org.skife.config.Config;
+import org.skife.config.Default;
+import org.skife.config.Description;
+import org.skife.config.TimeSpan;
+
+public interface UsageConfig extends KillbillConfig {
+
+    @Config("killbill.usage.timelines.length")
+    @Description("How long to buffer data in memory before flushing it to the database")
+    @Default("60m")
+    TimeSpan getTimelineLength();
+
+    // This is used to predict the number of samples between two times.  It might be
+    // better to store this information on a per event category basis.
+    @Config("killbill.usage.timelines.pollingInterval")
+    @Description("How long to between attribute polling.  This constant should be replaced by a flexible mechanism")
+    @Default("30s")
+    TimeSpan getPollingInterval();
+
+    @Config("killbill.usage.timelines.performForegroundWrites")
+    @Description("If true, perform database writes in the foreground; if false, in the background")
+    @Default("false")
+    boolean getPerformForegroundWrites();
+
+    @Config("killbill.usage.timelines.backgroundWriteBatchSize")
+    @Description("The number of TimelineChunks that must accumulate before we perform background writes, unless the max delay has been exceeded")
+    @Default("1000")
+    int getBackgroundWriteBatchSize();
+
+    @Config("killbill.usage.timelines.backgroundWriteCheckInterval")
+    @Description("The time interval between checks to see if we should perform background writes")
+    @Default("1s")
+    TimeSpan getBackgroundWriteCheckInterval();
+
+    @Config("killbill.usage.timelines.backgroundWriteMaxDelay")
+    @Description("The maximum timespan after pending chunks are added before we perform background writes")
+    @Default("1m")
+    TimeSpan getBackgroundWriteMaxDelay();
+
+    @Config("killbill.usage.timelines.timelineAggregationEnabled")
+    @Description("If true, periodically perform timeline aggregation; if false, don't aggregate")
+    @Default("true")
+    boolean getTimelineAggregationEnabled();
+
+    @Config("killbill.usage.timelines.maxAggregationLevel")
+    @Description("Max aggregation level")
+    @Default("5")
+    int getMaxAggregationLevel();
+
+    @Config("killbill.usage.timelines.chunksToAggregate")
+    @Description("A string with a comma-separated set of integers, one for each aggregation level, giving the number of sequential TimelineChunks with that aggregation level we must find to perform aggregation")
+    // These values translate to 4 hours, 16 hours, 2.7 days, 10.7 days, 42.7 days,
+    @Default("4,4,4,4,4")
+    String getChunksToAggregate();
+
+    @Config("killbill.usage.timelines.aggregationInterval")
+    @Description("How often to check to see if there are timelines ready to be aggregated")
+    @Default("2h")
+    TimeSpan getAggregationInterval();
+
+    @Config("killbill.usage.timelines.aggregationBatchSize")
+    @Description("The number of chunks to fetch in each batch processed")
+    @Default("4000")
+    int getAggregationBatchSize();
+
+    @Config("killbill.usage.timelines.aggregationSleepBetweenBatches")
+    @Description("How long to sleep between aggregation batches")
+    @Default("50ms")
+    TimeSpan getAggregationSleepBetweenBatches();
+
+    @Config("killbill.usage.timelines.maxChunkIdsToInvalidateOrDelete")
+    @Description("If the number of queued chunkIds to invalidate or delete is greater than or equal to this count, perform aggregated timeline writes and delete or invalidate the chunks aggregated")
+    @Default("1000")
+    int getMaxChunkIdsToInvalidateOrDelete();
+
+    @Config("killbill.usage.timelines.deleteAggregatedChunks")
+    @Description("If true, blast the old TimelineChunk rows; if false, leave them in peace, since they won't be accessed")
+    @Default("true")
+    boolean getDeleteAggregatedChunks();
+
+    @Config("killbill.usage.timelines.shutdownSaveMode")
+    @Description("What to save on shut down; either all timelines (save_all_timelines) or just the accumulator start times (save_start_times)")
+    @Default("save_all_timelines")
+    String getShutdownSaveMode();
+
+    @Config("killbill.usage.timelines.segmentsSize")
+    @Description("Direct memory segments size in bytes to allocate when buffering incoming events")
+    @Default("1048576")
+    int getSegmentsSize();
+
+    @Config("killbill.usage.timelines.maxNbSegments")
+    @Description("Max number of direct memory segments to allocate. This times the number of segments indicates the max amount of data buffered before storing a copy to disk")
+    @Default("10")
+    int getMaxNbSegments();
+
+    @Config("killbill.usage.timelines.spoolDir")
+    @Description("Spool directory for in-memory data")
+    @Default("/var/tmp/killbill")
+    String getSpoolDir();
+}
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java b/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
index ce09e8c..796717a 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/transfer/EntitlementTransferApi.java
@@ -19,11 +19,12 @@ import java.util.UUID;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.util.callcontext.CallContext;
 
 public interface EntitlementTransferApi {
 
-    public void transferBundle(final UUID sourceAccountId, final UUID destAccountId, final String bundleKey, final DateTime requestedDate, final boolean transferAddOn, final CallContext context)
+    public SubscriptionBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId, final String bundleKey, final DateTime requestedDate, final boolean transferAddOn, final CallContext context)
         throws EntitlementTransferApiException;
 
 }
diff --git a/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java b/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java
new file mode 100644
index 0000000..eb6935c
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/usage/api/UsageUserApi.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.api;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+
+public interface UsageUserApi {
+
+    /**
+     * Shortcut API to record a usage value of "1" for a given metric.
+     *
+     * @param bundleId   bundle id source
+     * @param metricName metric name for this usage
+     */
+    public void incrementUsage(final UUID bundleId, final String metricName);
+
+    /**
+     * Fine grained usage API if the external system doesn't roll its usage data. This is used to record e.g. "X has used
+     * 2 credits from his plan at 2012/02/04 4:12pm".
+     *
+     * @param bundleId   bundle id source
+     * @param metricName metric name for this usage
+     * @param timestamp  timestamp of this usage
+     * @param value      value to record
+     */
+    public void recordUsage(final UUID bundleId, final String metricName, final DateTime timestamp, final long value);
+
+    /**
+     * Bulk usage API if the external system rolls-up usage data. This is used to record e.g. "X has used 12 minutes
+     * of his data plan between 2012/02/04 and 2012/02/06".
+     *
+     * @param bundleId   bundle id source
+     * @param metricName metric name for this usage
+     * @param startDate  start date of the usage period
+     * @param endDate    end date of the usage period
+     * @param value      value to record
+     */
+    public void recordRolledUpUsage(final UUID bundleId, final String metricName, final DateTime startDate, final DateTime endDate, final long value);
+}
diff --git a/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java b/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java
new file mode 100644
index 0000000..1770335
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/util/api/AuditUserApi.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.ObjectType;
+
+public interface AuditUserApi {
+
+    /**
+     * Get all the audit entries for a given object
+     *
+     * @param objectId   the object id
+     * @param objectType the type of object
+     * @return all audit entries for that object
+     */
+    public List<AuditLog> getAuditLogs(final UUID objectId, final ObjectType objectType);
+}
diff --git a/api/src/main/java/com/ning/billing/util/audit/AuditLog.java b/api/src/main/java/com/ning/billing/util/audit/AuditLog.java
new file mode 100644
index 0000000..be89e7f
--- /dev/null
+++ b/api/src/main/java/com/ning/billing/util/audit/AuditLog.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.ChangeType;
+
+public interface AuditLog {
+
+    /**
+     * Get the type of change for this log entry
+     *
+     * @return the ChangeType
+     */
+    public ChangeType getChangeType();
+
+    /**
+     * Get the name of the requestor
+     *
+     * @return the requestor user name
+     */
+    public String getUserName();
+
+    /**
+     * Get the time when this change was effective
+     *
+     * @return the created date of this log entry
+     */
+    public DateTime getCreatedDate();
+
+    /**
+     * Get the reason code for this change
+     *
+     * @return the reason code
+     */
+    public String getReasonCode();
+
+    /**
+     * Get the user token of this change requestor
+     *
+     * @return the user token
+     */
+    public String getUserToken();
+
+    /**
+     * Get the comment for this change
+     *
+     * @return the comment
+     */
+    public String getComment();
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
index 6e3a0a9..99155bd 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/transfer/DefaultEntitlementTransferApi.java
@@ -199,12 +199,14 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
 
 
     @Override
-    public void transferBundle(final UUID sourceAccountId, final UUID destAccountId,
+    public SubscriptionBundle transferBundle(final UUID sourceAccountId, final UUID destAccountId,
             final String bundleKey, final DateTime transferDate, final boolean transferAddOn,
             final CallContext context) throws EntitlementTransferApiException {
 
         try {
 
+            final DateTime effectiveTransferDate = transferDate == null ? clock.getUTCNow() : transferDate;
+
             final SubscriptionBundle bundle = dao.getSubscriptionBundleFromAccountAndKey(sourceAccountId, bundleKey);
             if (bundle == null) {
                 throw new EntitlementTransferApiException(ErrorCode.ENT_CREATE_NO_BUNDLE, bundleKey);
@@ -213,7 +215,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
             // Get the bundle timeline for the old account
             final BundleTimeline bundleTimeline = timelineApi.getBundleTimeline(bundle);
 
-            final SubscriptionBundleData subscriptionBundleData = new SubscriptionBundleData(bundleKey, destAccountId, transferDate);
+            final SubscriptionBundleData subscriptionBundleData = new SubscriptionBundleData(bundleKey, destAccountId, effectiveTransferDate);
             final List<SubscriptionMigrationData> subscriptionMigrationDataList = new LinkedList<SubscriptionMigrationData>();
 
             final List<TransferCancelData> transferCancelDataList = new LinkedList<TransferCancelData>();
@@ -232,18 +234,16 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
                     }
                 } else {
 
-
-
                     // If BP or STANDALONE subscription, create the cancel event on effectiveCancelDate
-                    final DateTime effectiveCancelDate = oldSubscription.getChargedThroughDate() != null && transferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
-                            oldSubscription.getChargedThroughDate() : transferDate;
+                    final DateTime effectiveCancelDate = oldSubscription.getChargedThroughDate() != null && effectiveTransferDate.isBefore(oldSubscription.getChargedThroughDate()) ?
+                            oldSubscription.getChargedThroughDate() : effectiveTransferDate;
 
                             final EntitlementEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
                             .setSubscriptionId(cur.getId())
                             .setActiveVersion(cur.getActiveVersion())
                             .setProcessedDate(clock.getUTCNow())
                             .setEffectiveDate(effectiveCancelDate)
-                            .setRequestedDate(transferDate)
+                            .setRequestedDate(effectiveTransferDate)
                             .setUserToken(context.getUserToken())
                             .setFromDisk(true));
 
@@ -262,11 +262,11 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
                 .setId(UUID.randomUUID())
                 .setBundleId(subscriptionBundleData.getId())
                 .setCategory(productCategory)
-                .setBundleStartDate(transferDate)
+                .setBundleStartDate(effectiveTransferDate)
                 .setAlignStartDate(subscriptionAlignStartDate),
                 ImmutableList.<EntitlementEvent>of());
 
-                final List<EntitlementEvent> events = toEvents(existingEvents, subscriptionData, transferDate, context);
+                final List<EntitlementEvent> events = toEvents(existingEvents, subscriptionData, effectiveTransferDate, context);
                 final SubscriptionMigrationData curData = new SubscriptionMigrationData(subscriptionData, events);
                 subscriptionMigrationDataList.add(curData);
             }
@@ -275,6 +275,7 @@ public class DefaultEntitlementTransferApi implements EntitlementTransferApi {
             // Atomically cancel all subscription on old account and create new bundle, subscriptions, events for new account
             dao.transfer(sourceAccountId, destAccountId, bundleMigrationData, transferCancelDataList, context);
 
+            return bundle;
         } catch (EntitlementRepairException e) {
             throw new EntitlementTransferApiException(e);
         }
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
index b2816ce..455fe64 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/AuditedEntitlementDao.java
@@ -567,7 +567,7 @@ public class AuditedEntitlementDao implements EntitlementDao {
                     final String baseProductName = (futureBaseEvent instanceof ApiEventChange) ?
                             ((ApiEventChange) futureBaseEvent).getEventPlan() : null;
 
-                    final boolean createCancelEvent = (futureBaseEvent != null) &&
+                    final boolean createCancelEvent = (futureBaseEvent != null && targetAddOnPlan != null) &&
                             ((futureBaseEvent instanceof ApiEventCancel) ||
                                     ((!addonUtils.isAddonAvailableFromPlanName(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan)) ||
                                             (addonUtils.isAddonIncludedFromPlanName(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan))));
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
index 225bcd8..29b1d29 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AccountTimelineJson.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
@@ -31,6 +32,7 @@ import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.payment.api.Payment;
 import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.audit.AuditLog;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -86,11 +88,16 @@ public class AccountTimelineJson {
     }
 
     public AccountTimelineJson(final Account account, final List<Invoice> invoices, final List<Payment> payments, final List<BundleTimeline> bundles,
-                               final Multimap<UUID, Refund> refundsByPayment, final Multimap<UUID, InvoicePayment> chargebacksByPayment) {
+                               final Multimap<UUID, Refund> refundsByPayment, final Multimap<UUID, InvoicePayment> chargebacksByPayment,
+                               final Map<UUID, List<AuditLog>> invoiceAuditLogs, final Map<UUID, List<AuditLog>> invoiceItemsAuditLogs,
+                               final Map<UUID, List<AuditLog>> paymentsAuditLogs, final Map<UUID, List<AuditLog>> refundsAuditLogs,
+                               final Map<UUID, List<AuditLog>> chargebacksAuditLogs, final Map<UUID, List<AuditLog>> bundlesAuditLogs) {
         this.account = new AccountJsonSimple(account.getId().toString(), account.getExternalKey());
         this.bundles = new LinkedList<BundleJsonWithSubscriptions>();
-        for (final BundleTimeline cur : bundles) {
-            this.bundles.add(new BundleJsonWithSubscriptions(account.getId(), cur));
+        for (final BundleTimeline bundle : bundles) {
+            final List<AuditLog> auditLogs = bundlesAuditLogs.get(bundle.getBundleId());
+            final BundleJsonWithSubscriptions jsonWithSubscriptions = new BundleJsonWithSubscriptions(account.getId(), bundle, auditLogs);
+            this.bundles.add(jsonWithSubscriptions);
         }
 
         this.invoices = new LinkedList<InvoiceJsonWithBundleKeys>();
@@ -99,50 +106,45 @@ public class AccountTimelineJson {
         for (final Invoice invoice : invoices) {
             for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
                 if (InvoiceItemType.CREDIT_ADJ.equals(invoiceItem.getInvoiceItemType())) {
-                    credits.add(new CreditJson(invoiceItem, account.getTimeZone()));
+                    final List<AuditLog> auditLogs = invoiceItemsAuditLogs.get(invoiceItem.getId());
+                    credits.add(new CreditJson(invoiceItem, account.getTimeZone(), auditLogs));
                 }
             }
         }
         // Create now the invoice json objects
         for (final Invoice invoice : invoices) {
-            this.invoices.add(new InvoiceJsonWithBundleKeys(invoice.getPaidAmount(),
-                                                            invoice.getCBAAmount(),
-                                                            invoice.getCreditAdjAmount(),
-                                                            invoice.getRefundAdjAmount(),
-                                                            invoice.getId().toString(),
-                                                            invoice.getInvoiceDate(),
-                                                            invoice.getTargetDate(),
-                                                            Integer.toString(invoice.getInvoiceNumber()),
-                                                            invoice.getBalance(),
-                                                            invoice.getAccountId().toString(),
+            final List<AuditLog> auditLogs = invoiceAuditLogs.get(invoice.getId());
+            this.invoices.add(new InvoiceJsonWithBundleKeys(invoice,
                                                             getBundleExternalKey(invoice, bundles),
-                                                            credits));
+                                                            credits,
+                                                            auditLogs));
         }
 
         this.payments = new LinkedList<PaymentJsonWithBundleKeys>();
         for (final Payment payment : payments) {
             final List<RefundJson> refunds = new ArrayList<RefundJson>();
             for (final Refund refund : refundsByPayment.get(payment.getId())) {
-                refunds.add(new RefundJson(refund));
+                final List<AuditLog> auditLogs = refundsAuditLogs.get(refund.getId());
+                refunds.add(new RefundJson(refund, auditLogs));
             }
 
             final List<ChargebackJson> chargebacks = new ArrayList<ChargebackJson>();
             for (final InvoicePayment chargeback : chargebacksByPayment.get(payment.getId())) {
-                chargebacks.add(new ChargebackJson(chargeback));
+                final List<AuditLog> auditLogs = chargebacksAuditLogs.get(chargeback.getId());
+                chargebacks.add(new ChargebackJson(chargeback, auditLogs));
             }
 
-            final int paymentAttemptSize = payment.getAttempts().size();
+            final int nbOfPaymentAttempts = payment.getAttempts().size();
             final String status = payment.getPaymentStatus().toString();
-            this.payments.add(new PaymentJsonWithBundleKeys(payment.getAmount(), payment.getPaidAmount(), account.getId().toString(),
-                                                            payment.getInvoiceId().toString(), payment.getId().toString(),
-                                                            payment.getPaymentMethodId().toString(),
-                                                            payment.getEffectiveDate(), payment.getEffectiveDate(),
-                                                            paymentAttemptSize, payment.getCurrency().toString(), status,
-                                                            payment.getAttempts().get(paymentAttemptSize - 1).getGatewayErrorCode(),
-                                                            payment.getAttempts().get(paymentAttemptSize - 1).getGatewayErrorMsg(),
-                                                            payment.getExtFirstPaymentIdRef(), payment.getExtSecondPaymentIdRef(),
+            final List<AuditLog> auditLogs = paymentsAuditLogs.get(payment.getId());
+            this.payments.add(new PaymentJsonWithBundleKeys(payment,
+                                                            status,
+                                                            nbOfPaymentAttempts,
                                                             getBundleExternalKey(payment.getInvoiceId(), invoices, bundles),
-                                                            refunds, chargebacks));
+                                                            account.getId(),
+                                                            refunds,
+                                                            chargebacks,
+                                                            auditLogs));
         }
     }
 
@@ -176,15 +178,27 @@ public class AccountTimelineJson {
 
     @Override
     public boolean equals(final Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         final AccountTimelineJson that = (AccountTimelineJson) o;
 
-        if (account != null ? !account.equals(that.account) : that.account != null) return false;
-        if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) return false;
-        if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) return false;
-        if (payments != null ? !payments.equals(that.payments) : that.payments != null) return false;
+        if (account != null ? !account.equals(that.account) : that.account != null) {
+            return false;
+        }
+        if (bundles != null ? !bundles.equals(that.bundles) : that.bundles != null) {
+            return false;
+        }
+        if (invoices != null ? !invoices.equals(that.invoices) : that.invoices != null) {
+            return false;
+        }
+        if (payments != null ? !payments.equals(that.payments) : that.payments != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java
new file mode 100644
index 0000000..333a7d4
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/AuditLogJson.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AuditLogJson {
+
+    private final String changeType;
+    private final DateTime changeDate;
+    private final String changedBy;
+    private final String reasonCode;
+    private final String comments;
+    private final String userToken;
+
+    @JsonCreator
+    public AuditLogJson(@JsonProperty("changeType") final String changeType,
+                        @JsonProperty("changeDate") final DateTime changeDate,
+                        @JsonProperty("changedBy") final String changedBy,
+                        @JsonProperty("reasonCode") final String reasonCode,
+                        @JsonProperty("comments") final String comments,
+                        @JsonProperty("userToken") final String userToken) {
+        this.changeType = changeType;
+        this.changeDate = changeDate;
+        this.changedBy = changedBy;
+        this.reasonCode = reasonCode;
+        this.comments = comments;
+        this.userToken = userToken;
+    }
+
+    public AuditLogJson(final AuditLog auditLog) {
+        this(auditLog.getChangeType().toString(), auditLog.getCreatedDate(), auditLog.getUserName(), auditLog.getReasonCode(),
+             auditLog.getComment(), auditLog.getUserToken());
+    }
+
+    public String getChangeType() {
+        return changeType;
+    }
+
+    public DateTime getChangeDate() {
+        return changeDate;
+    }
+
+    public String getChangedBy() {
+        return changedBy;
+    }
+
+    public String getReasonCode() {
+        return reasonCode;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public String getUserToken() {
+        return userToken;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("AuditLogJson");
+        sb.append("{changeType='").append(changeType).append('\'');
+        sb.append(", changeDate=").append(changeDate);
+        sb.append(", changedBy=").append(changedBy);
+        sb.append(", reasonCode='").append(reasonCode).append('\'');
+        sb.append(", comments='").append(comments).append('\'');
+        sb.append(", userToken='").append(userToken).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final AuditLogJson that = (AuditLogJson) o;
+
+        if (changeDate != null ? changeDate.compareTo(that.changeDate) != 0 : that.changeDate != null) {
+            return false;
+        }
+        if (changeType != null ? !changeType.equals(that.changeType) : that.changeType != null) {
+            return false;
+        }
+        if (changedBy != null ? !changedBy.equals(that.changedBy) : that.changedBy != null) {
+            return false;
+        }
+        if (comments != null ? !comments.equals(that.comments) : that.comments != null) {
+            return false;
+        }
+        if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+            return false;
+        }
+        if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = changeType != null ? changeType.hashCode() : 0;
+        result = 31 * result + (changeDate != null ? changeDate.hashCode() : 0);
+        result = 31 * result + (changedBy != null ? changedBy.hashCode() : 0);
+        result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+        result = 31 * result + (comments != null ? comments.hashCode() : 0);
+        result = 31 * result + (userToken != null ? userToken.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
index cead496..ded56fe 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonNoSubscriptions.java
@@ -16,12 +16,14 @@
 
 package com.ning.billing.jaxrs.json;
 
-import javax.annotation.Nullable;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
+import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 
 public class BundleJsonNoSubscriptions extends BundleJsonSimple {
 
@@ -31,8 +33,9 @@ public class BundleJsonNoSubscriptions extends BundleJsonSimple {
     public BundleJsonNoSubscriptions(@JsonProperty("bundleId") final String bundleId,
                                      @JsonProperty("accountId") final String accountId,
                                      @JsonProperty("externalKey") final String externalKey,
-                                     @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions) {
-        super(bundleId, externalKey);
+                                     @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions,
+                                     @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(bundleId, externalKey, auditLogs);
         this.accountId = accountId;
     }
 
@@ -41,25 +44,20 @@ public class BundleJsonNoSubscriptions extends BundleJsonSimple {
     }
 
     public BundleJsonNoSubscriptions(final SubscriptionBundle bundle) {
-        super(bundle.getId().toString(), bundle.getKey());
+        super(bundle.getId().toString(), bundle.getKey(), null);
         this.accountId = bundle.getAccountId().toString();
     }
 
-    public BundleJsonNoSubscriptions() {
-        super(null, null);
-        this.accountId = null;
-    }
-
     @Override
     public int hashCode() {
         final int prime = 31;
         int result = 1;
         result = prime * result
-                + ((accountId == null) ? 0 : accountId.hashCode());
+                 + ((accountId == null) ? 0 : accountId.hashCode());
         result = prime * result
-                + ((bundleId == null) ? 0 : bundleId.hashCode());
+                 + ((bundleId == null) ? 0 : bundleId.hashCode());
         result = prime * result
-                + ((externalKey == null) ? 0 : externalKey.hashCode());
+                 + ((externalKey == null) ? 0 : externalKey.hashCode());
         return result;
     }
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
index 8c88959..08fcc40 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonSimple.java
@@ -13,14 +13,20 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.jaxrs.json;
 
+import java.util.List;
+import java.util.UUID;
+
 import javax.annotation.Nullable;
 
+import com.ning.billing.util.audit.AuditLog;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class BundleJsonSimple {
+public class BundleJsonSimple extends JsonBase {
 
     protected final String bundleId;
 
@@ -28,13 +34,15 @@ public class BundleJsonSimple {
 
     @JsonCreator
     public BundleJsonSimple(@JsonProperty("bundleId") @Nullable final String bundleId,
-                            @JsonProperty("externalKey") @Nullable final String externalKey) {
+                            @JsonProperty("externalKey") @Nullable final String externalKey,
+                            @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.bundleId = bundleId;
         this.externalKey = externalKey;
     }
 
-    public BundleJsonSimple() {
-        this(null, null);
+    public BundleJsonSimple(final UUID bundleId, final String externalKey, final List<AuditLog> auditLogs) {
+        this(bundleId.toString(), externalKey, toAuditLogJson(auditLogs));
     }
 
     @JsonProperty("bundleId")
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
index c2ca338..be03ceb 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/BundleJsonWithSubscriptions.java
@@ -13,18 +13,21 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.jaxrs.json;
 
-import javax.annotation.Nullable;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.Nullable;
+
 import com.ning.billing.entitlement.api.timeline.BundleTimeline;
 import com.ning.billing.entitlement.api.timeline.SubscriptionTimeline;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.audit.AuditLog;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class BundleJsonWithSubscriptions extends BundleJsonSimple {
 
@@ -33,8 +36,9 @@ public class BundleJsonWithSubscriptions extends BundleJsonSimple {
     @JsonCreator
     public BundleJsonWithSubscriptions(@JsonProperty("bundleId") @Nullable final String bundleId,
                                        @JsonProperty("externalKey") @Nullable final String externalKey,
-                                       @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions) {
-        super(bundleId, externalKey);
+                                       @JsonProperty("subscriptions") @Nullable final List<SubscriptionJsonWithEvents> subscriptions,
+                                       @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(bundleId, externalKey, auditLogs);
         this.subscriptions = subscriptions;
     }
 
@@ -43,23 +47,14 @@ public class BundleJsonWithSubscriptions extends BundleJsonSimple {
         return subscriptions;
     }
 
-    public BundleJsonWithSubscriptions(@Nullable final UUID accountId, final BundleTimeline bundle) {
-        super(bundle.getBundleId().toString(), bundle.getExternalKey());
+    public BundleJsonWithSubscriptions(@Nullable final UUID accountId, final BundleTimeline bundle, final List<AuditLog> auditLogs) {
+        super(bundle.getBundleId(), bundle.getExternalKey(), auditLogs);
         this.subscriptions = new LinkedList<SubscriptionJsonWithEvents>();
         for (final SubscriptionTimeline cur : bundle.getSubscriptions()) {
             this.subscriptions.add(new SubscriptionJsonWithEvents(bundle.getBundleId(), cur));
         }
     }
 
-    public BundleJsonWithSubscriptions(final SubscriptionBundle bundle) {
-        super(bundle.getId().toString(), bundle.getKey());
-        this.subscriptions = null;
-    }
-
-    public BundleJsonWithSubscriptions() {
-        this(null, null, null);
-    }
-
     @Override
     public boolean equals(final Object o) {
         if (this == o) {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
index 9eba9a9..dec2fd6 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/ChargebackJson.java
@@ -17,15 +17,20 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.invoice.api.InvoicePayment;
+import com.ning.billing.util.audit.AuditLog;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.ning.billing.invoice.api.InvoicePayment;
 
-// TODO: populate reason code, requested date from audit log
-public class ChargebackJson {
+public class ChargebackJson extends JsonBase {
+
     private final DateTime requestedDate;
     private final DateTime effectiveDate;
     private final BigDecimal chargebackAmount;
@@ -37,7 +42,9 @@ public class ChargebackJson {
                           @JsonProperty("effectiveDate") final DateTime effectiveDate,
                           @JsonProperty("chargebackAmount") final BigDecimal chargebackAmount,
                           @JsonProperty("paymentId") final String paymentId,
-                          @JsonProperty("reason") final String reason) {
+                          @JsonProperty("reason") final String reason,
+                          @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.requestedDate = requestedDate;
         this.effectiveDate = effectiveDate;
         this.chargebackAmount = chargebackAmount;
@@ -46,11 +53,12 @@ public class ChargebackJson {
     }
 
     public ChargebackJson(final InvoicePayment chargeback) {
-        this.requestedDate = null;
-        this.effectiveDate = chargeback.getPaymentDate();
-        this.chargebackAmount = chargeback.getAmount().negate();
-        this.paymentId = chargeback.getPaymentId().toString();
-        this.reason = null;
+        this(chargeback, null);
+    }
+
+    public ChargebackJson(final InvoicePayment chargeback, @Nullable final List<AuditLog> auditLogs) {
+        this(chargeback.getPaymentDate(), chargeback.getPaymentDate(), chargeback.getAmount().negate(),
+             chargeback.getPaymentId().toString(), reasonCodeFromAuditLogs(auditLogs), toAuditLogJson(auditLogs));
     }
 
     public DateTime getRequestedDate() {
@@ -85,11 +93,11 @@ public class ChargebackJson {
         final ChargebackJson that = (ChargebackJson) o;
 
         if (!((chargebackAmount == null && that.chargebackAmount == null) ||
-                (chargebackAmount != null && that.chargebackAmount != null && chargebackAmount.compareTo(that.chargebackAmount) == 0))) {
+              (chargebackAmount != null && that.chargebackAmount != null && chargebackAmount.compareTo(that.chargebackAmount) == 0))) {
             return false;
         }
         if (!((effectiveDate == null && that.effectiveDate == null) ||
-                (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+              (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
             return false;
         }
         if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
@@ -99,7 +107,7 @@ public class ChargebackJson {
             return false;
         }
         if (!((requestedDate == null && that.requestedDate == null) ||
-                (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
+              (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
             return false;
         }
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
index ab5221e..458e277 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/CreditJson.java
@@ -17,17 +17,21 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 
 import com.ning.billing.invoice.api.InvoiceItem;
+import com.ning.billing.util.audit.AuditLog;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class CreditJson {
+public class CreditJson extends JsonBase {
 
     private final BigDecimal creditAmount;
     private final UUID invoiceId;
@@ -44,7 +48,9 @@ public class CreditJson {
                       @JsonProperty("requestedDate") final DateTime requestedDate,
                       @JsonProperty("effectiveDate") final DateTime effectiveDate,
                       @JsonProperty("reason") final String reason,
-                      @JsonProperty("accountId") final UUID accountId) {
+                      @JsonProperty("accountId") final UUID accountId,
+                      @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.creditAmount = creditAmount;
         this.invoiceId = invoiceId;
         this.invoiceNumber = invoiceNumber;
@@ -54,7 +60,8 @@ public class CreditJson {
         this.accountId = accountId;
     }
 
-    public CreditJson(final InvoiceItem credit, final DateTimeZone accountTimeZone) {
+    public CreditJson(final InvoiceItem credit, final DateTimeZone accountTimeZone, final List<AuditLog> auditLogs) {
+        super(toAuditLogJson(auditLogs));
         this.creditAmount = credit.getAmount();
         this.invoiceId = credit.getInvoiceId();
         this.invoiceNumber = null;
@@ -64,6 +71,10 @@ public class CreditJson {
         this.accountId = credit.getAccountId();
     }
 
+    public CreditJson(final InvoiceItem credit, final DateTimeZone timeZone) {
+        this(credit, timeZone, null);
+    }
+
     public BigDecimal getCreditAmount() {
         return creditAmount;
     }
@@ -93,6 +104,21 @@ public class CreditJson {
     }
 
     @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("CreditJson");
+        sb.append("{creditAmount=").append(creditAmount);
+        sb.append(", invoiceId=").append(invoiceId);
+        sb.append(", invoiceNumber='").append(invoiceNumber).append('\'');
+        sb.append(", requestedDate=").append(requestedDate);
+        sb.append(", effectiveDate=").append(effectiveDate);
+        sb.append(", reason='").append(reason).append('\'');
+        sb.append(", accountId=").append(accountId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
     public boolean equals(final Object o) {
         if (this == o) {
             return true;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
index 195c7d4..7775008 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonSimple.java
@@ -17,17 +17,19 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 
 import javax.annotation.Nullable;
 
 import org.joda.time.LocalDate;
 
 import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.util.audit.AuditLog;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class InvoiceJsonSimple {
+public class InvoiceJsonSimple extends JsonBase {
 
     private final BigDecimal amount;
     private final String invoiceId;
@@ -40,10 +42,6 @@ public class InvoiceJsonSimple {
     private final BigDecimal cba;
     private final String accountId;
 
-    public InvoiceJsonSimple() {
-        this(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, null, null, null, null, BigDecimal.ZERO, null);
-    }
-
     @JsonCreator
     public InvoiceJsonSimple(@JsonProperty("amount") final BigDecimal amount,
                              @JsonProperty("cba") final BigDecimal cba,
@@ -54,7 +52,9 @@ public class InvoiceJsonSimple {
                              @JsonProperty("targetDate") @Nullable final LocalDate targetDate,
                              @JsonProperty("invoiceNumber") @Nullable final String invoiceNumber,
                              @JsonProperty("balance") final BigDecimal balance,
-                             @JsonProperty("accountId") @Nullable final String accountId) {
+                             @JsonProperty("accountId") @Nullable final String accountId,
+                             @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.amount = amount;
         this.cba = cba;
         this.creditAdj = creditAdj;
@@ -67,9 +67,14 @@ public class InvoiceJsonSimple {
         this.accountId = accountId;
     }
 
+    public InvoiceJsonSimple(final Invoice input, final List<AuditLog> auditLogs) {
+        this(input.getChargedAmount(), input.getCBAAmount(), input.getCreditAdjAmount(), input.getRefundAdjAmount(),
+             input.getId().toString(), input.getInvoiceDate(), input.getTargetDate(), String.valueOf(input.getInvoiceNumber()),
+             input.getBalance(), input.getAccountId().toString(), toAuditLogJson(auditLogs));
+    }
+
     public InvoiceJsonSimple(final Invoice input) {
-        this(input.getChargedAmount(), input.getCBAAmount(), input.getCreditAdjAmount(), input.getRefundAdjAmount(), input.getId().toString(), input.getInvoiceDate(),
-             input.getTargetDate(), String.valueOf(input.getInvoiceNumber()), input.getBalance(), input.getAccountId().toString());
+        this(input, null);
     }
 
     public BigDecimal getAmount() {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
index 5279507..e07729a 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithBundleKeys.java
@@ -19,25 +19,21 @@ package com.ning.billing.jaxrs.json;
 import java.math.BigDecimal;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.LocalDate;
 
 import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.util.audit.AuditLog;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.ImmutableList;
 
 public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
 
     private final String bundleKeys;
     private final List<CreditJson> credits;
 
-    public InvoiceJsonWithBundleKeys() {
-        super();
-        this.bundleKeys = null;
-        this.credits = ImmutableList.<CreditJson>of();
-    }
-
     @JsonCreator
     public InvoiceJsonWithBundleKeys(@JsonProperty("amount") final BigDecimal amount,
                                      @JsonProperty("cba") final BigDecimal cba,
@@ -50,14 +46,15 @@ public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
                                      @JsonProperty("balance") final BigDecimal balance,
                                      @JsonProperty("accountId") final String accountId,
                                      @JsonProperty("externalBundleKeys") final String bundleKeys,
-                                     @JsonProperty("credits") final List<CreditJson> credits) {
-        super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId);
+                                     @JsonProperty("credits") final List<CreditJson> credits,
+                                     @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId, auditLogs);
         this.bundleKeys = bundleKeys;
         this.credits = credits;
     }
 
-    public InvoiceJsonWithBundleKeys(final Invoice input, final String bundleKeys, final List<CreditJson> credits) {
-        super(input);
+    public InvoiceJsonWithBundleKeys(final Invoice input, final String bundleKeys, final List<CreditJson> credits, final List<AuditLog> auditLogs) {
+        super(input, auditLogs);
         this.bundleKeys = bundleKeys;
         this.credits = credits;
     }
@@ -82,14 +79,24 @@ public class InvoiceJsonWithBundleKeys extends InvoiceJsonSimple {
 
     @Override
     public boolean equals(final Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        if (!super.equals(o)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
 
         final InvoiceJsonWithBundleKeys that = (InvoiceJsonWithBundleKeys) o;
 
-        if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) return false;
-        if (credits != null ? !credits.equals(that.credits) : that.credits != null) return false;
+        if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+            return false;
+        }
+        if (credits != null ? !credits.equals(that.credits) : that.credits != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
index 0f73274..b866f3c 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/InvoiceJsonWithItems.java
@@ -21,6 +21,8 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.LocalDate;
 
 import com.ning.billing.invoice.api.Invoice;
@@ -44,8 +46,9 @@ public class InvoiceJsonWithItems extends InvoiceJsonSimple {
                                 @JsonProperty("invoiceNumber") final String invoiceNumber,
                                 @JsonProperty("balance") final BigDecimal balance,
                                 @JsonProperty("accountId") final String accountId,
-                                @JsonProperty("items") final List<InvoiceItemJsonSimple> items) {
-        super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId);
+                                @JsonProperty("items") final List<InvoiceItemJsonSimple> items,
+                                @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate, targetDate, invoiceNumber, balance, accountId, auditLogs);
         this.items = new ArrayList<InvoiceItemJsonSimple>(items);
     }
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java
new file mode 100644
index 0000000..b3e078c
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/JsonBase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.ning.billing.util.audit.AuditLog;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+
+public abstract class JsonBase {
+
+    protected List<AuditLogJson> auditLogs;
+
+    public JsonBase(@Nullable final List<AuditLogJson> auditLogs) {
+        this.auditLogs = auditLogs;
+    }
+
+    protected static ImmutableList<AuditLogJson> toAuditLogJson(@Nullable final List<AuditLog> auditLogs) {
+        if (auditLogs == null) {
+            return null;
+        }
+
+        return ImmutableList.<AuditLogJson>copyOf(Collections2.transform(auditLogs, new Function<AuditLog, AuditLogJson>() {
+            @Override
+            public AuditLogJson apply(@Nullable final AuditLog input) {
+                return new AuditLogJson(input);
+            }
+        }));
+    }
+
+    protected static String reasonCodeFromAuditLogs(@Nullable final List<AuditLog> auditLogs) {
+        if (auditLogs == null || auditLogs.size() == 0) {
+            return null;
+        }
+
+        return auditLogs.get(0).getReasonCode();
+    }
+
+    public List<AuditLogJson> getAuditLogs() {
+        return auditLogs;
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
index 85fb420..360202b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonSimple.java
@@ -19,15 +19,19 @@ package com.ning.billing.jaxrs.json;
 import java.math.BigDecimal;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.joda.time.DateTime;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.ning.billing.payment.api.Payment;
-import com.ning.billing.payment.api.Payment.PaymentAttempt;
+import com.ning.billing.util.audit.AuditLog;
 import com.ning.billing.util.clock.DefaultClock;
 
-public class PaymentJsonSimple {
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PaymentJsonSimple extends JsonBase {
+
     private final BigDecimal paidAmount;
 
     private final BigDecimal amount;
@@ -58,25 +62,6 @@ public class PaymentJsonSimple {
 
     private final String extSecondPaymentIdRef;
 
-    public PaymentJsonSimple() {
-        this.amount = null;
-        this.paidAmount = null;
-        this.invoiceId = null;
-        this.accountId = null;
-        this.paymentId = null;
-        this.paymentMethodId = null;
-        this.requestedDate = null;
-        this.effectiveDate = null;
-        this.currency = null;
-        this.retryCount = null;
-        this.status = null;
-        this.gatewayErrorCode = null;
-        this.gatewayErrorMsg = null;
-        this.extFirstPaymentIdRef = null;
-        this.extSecondPaymentIdRef = null;
-
-    }
-
     @JsonCreator
     public PaymentJsonSimple(@JsonProperty("amount") final BigDecimal amount,
                              @JsonProperty("paidAmount") final BigDecimal paidAmount,
@@ -92,8 +77,9 @@ public class PaymentJsonSimple {
                              @JsonProperty("gatewayErrorCode") final String gatewayErrorCode,
                              @JsonProperty("gatewayErrorMsg") final String gatewayErrorMsg,
                              @JsonProperty("extFirstPaymentIdRef") final String extFirstPaymentIdRef,
-                             @JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef) {
-        super();
+                             @JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef,
+                             @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.amount = amount;
         this.paidAmount = paidAmount;
         this.invoiceId = invoiceId;
@@ -111,22 +97,16 @@ public class PaymentJsonSimple {
         this.extSecondPaymentIdRef = extSecondPaymentIdRef;
     }
 
-    public PaymentJsonSimple(final Payment src) {
-        this.amount = src.getAmount();
-        this.paidAmount =  src.getPaidAmount();
-        this.invoiceId = src.getInvoiceId().toString();
-        this.accountId = src.getAccountId().toString();
-        this.paymentId = src.getId().toString();
-        this.paymentMethodId =src.getPaymentMethodId().toString();
-        this.requestedDate = src.getEffectiveDate();
-        this.effectiveDate = src.getEffectiveDate();
-        this.currency = src.getCurrency().toString();
-        this.retryCount = src.getAttempts().size();
-        this.gatewayErrorCode = src.getAttempts().get(retryCount - 1).getGatewayErrorCode();
-        this.gatewayErrorMsg = src.getAttempts().get(retryCount - 1).getGatewayErrorMsg();;
-        this.status = src.getPaymentStatus().toString();
-        this.extFirstPaymentIdRef = src.getExtFirstPaymentIdRef();
-        this.extSecondPaymentIdRef = src.getExtSecondPaymentIdRef();
+    public PaymentJsonSimple(final Payment src, final List<AuditLog> auditLogs) {
+        this(src.getAmount(), src.getPaidAmount(), src.getAccountId().toString(), src.getInvoiceId().toString(),
+             src.getId().toString(), src.getPaymentMethodId().toString(), src.getEffectiveDate(), src.getEffectiveDate(),
+             src.getAttempts().size(), src.getCurrency().toString(), src.getPaymentStatus().toString(),
+             src.getAttempts().get(src.getAttempts().size() - 1).getGatewayErrorCode(), src.getAttempts().get(src.getAttempts().size() - 1).getGatewayErrorMsg(),
+             src.getExtFirstPaymentIdRef(), src.getExtSecondPaymentIdRef(), toAuditLogJson(auditLogs));
+    }
+
+    public PaymentJsonSimple(final Payment payment) {
+        this(payment, null);
     }
 
     public BigDecimal getPaidAmount() {
@@ -137,7 +117,6 @@ public class PaymentJsonSimple {
         return invoiceId;
     }
 
-
     public String getPaymentId() {
         return paymentId;
     }
@@ -205,28 +184,28 @@ public class PaymentJsonSimple {
             return false;
         }
         if (!((amount == null && that.amount == null) ||
-                (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
+              (amount != null && that.amount != null && amount.compareTo(that.amount) == 0))) {
             return false;
         }
         if (currency != null ? !currency.equals(that.currency) : that.currency != null) {
             return false;
         }
         if (!((effectiveDate == null && that.effectiveDate == null) ||
-                (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
+              (effectiveDate != null && that.effectiveDate != null && effectiveDate.compareTo(that.effectiveDate) == 0))) {
             return false;
         }
         if (invoiceId != null ? !invoiceId.equals(that.invoiceId) : that.invoiceId != null) {
             return false;
         }
         if (!((paidAmount == null && that.paidAmount == null) ||
-                (paidAmount != null && that.paidAmount != null && paidAmount.compareTo(that.paidAmount) == 0))) {
+              (paidAmount != null && that.paidAmount != null && paidAmount.compareTo(that.paidAmount) == 0))) {
             return false;
         }
         if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
             return false;
         }
         if (!((requestedDate == null && that.requestedDate == null) ||
-                (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
+              (requestedDate != null && that.requestedDate != null && requestedDate.compareTo(that.requestedDate) == 0))) {
             return false;
         }
         if (retryCount != null ? !retryCount.equals(that.retryCount) : that.retryCount != null) {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
index ec87e78..d7029dc 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentJsonWithBundleKeys.java
@@ -18,12 +18,17 @@ package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
 import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 
+import com.ning.billing.payment.api.Payment;
+import com.ning.billing.util.audit.AuditLog;
+
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.collect.ImmutableList;
 
 public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
 
@@ -31,13 +36,6 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
     private final List<RefundJson> refunds;
     private final List<ChargebackJson> chargebacks;
 
-    public PaymentJsonWithBundleKeys() {
-        super();
-        this.bundleKeys = null;
-        this.refunds = ImmutableList.<RefundJson>of();
-        this.chargebacks = ImmutableList.<ChargebackJson>of();
-    }
-
     @JsonCreator
     public PaymentJsonWithBundleKeys(@JsonProperty("amount") final BigDecimal amount,
                                      @JsonProperty("paidAmount") final BigDecimal paidAmount,
@@ -56,14 +54,30 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
                                      @JsonProperty("extSecondPaymentIdRef") final String extSecondPaymentIdRef,
                                      @JsonProperty("externalBundleKeys") final String bundleKeys,
                                      @JsonProperty("refunds") final List<RefundJson> refunds,
-                                     @JsonProperty("chargebacks") final List<ChargebackJson> chargebacks) {
-        super(amount, paidAmount, accountId, invoiceId, paymentId, paymentMethodId, requestedDate, effectiveDate, retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg,
-                extFirstPaymentIdRef, extSecondPaymentIdRef);
+                                     @JsonProperty("chargebacks") final List<ChargebackJson> chargebacks,
+                                     @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(amount, paidAmount, accountId, invoiceId, paymentId, paymentMethodId, requestedDate, effectiveDate,
+              retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef,
+              extSecondPaymentIdRef, auditLogs);
         this.bundleKeys = bundleKeys;
         this.refunds = refunds;
         this.chargebacks = chargebacks;
     }
 
+    public PaymentJsonWithBundleKeys(final Payment payment, final String status, final int nbOfPaymentAttempts, final String bundleExternalKey,
+                                     final UUID accountId, final List<RefundJson> refunds, final List<ChargebackJson> chargebacks,
+                                     final List<AuditLog> auditLogs) {
+        this(payment.getAmount(), payment.getPaidAmount(), accountId.toString(),
+             payment.getInvoiceId().toString(), payment.getId().toString(),
+             payment.getPaymentMethodId().toString(),
+             payment.getEffectiveDate(), payment.getEffectiveDate(),
+             nbOfPaymentAttempts, payment.getCurrency().toString(), status,
+             payment.getAttempts().get(nbOfPaymentAttempts - 1).getGatewayErrorCode(),
+             payment.getAttempts().get(nbOfPaymentAttempts - 1).getGatewayErrorMsg(),
+             payment.getExtFirstPaymentIdRef(), payment.getExtSecondPaymentIdRef(),
+             bundleExternalKey, refunds, chargebacks, toAuditLogJson(auditLogs));
+    }
+
     public String getBundleKeys() {
         return bundleKeys;
     }
@@ -89,15 +103,27 @@ public class PaymentJsonWithBundleKeys extends PaymentJsonSimple {
 
     @Override
     public boolean equals(final Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        if (!super.equals(o)) return false;
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
 
         final PaymentJsonWithBundleKeys that = (PaymentJsonWithBundleKeys) o;
 
-        if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) return false;
-        if (chargebacks != null ? !chargebacks.equals(that.chargebacks) : that.chargebacks != null) return false;
-        if (refunds != null ? !refunds.equals(that.refunds) : that.refunds != null) return false;
+        if (bundleKeys != null ? !bundleKeys.equals(that.bundleKeys) : that.bundleKeys != null) {
+            return false;
+        }
+        if (chargebacks != null ? !chargebacks.equals(that.chargebacks) : that.chargebacks != null) {
+            return false;
+        }
+        if (refunds != null ? !refunds.equals(that.refunds) : that.refunds != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
index ff1547a..390837b 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/RefundJson.java
@@ -17,15 +17,19 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
+
+import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 
 import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.audit.AuditLog;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-public class RefundJson {
+public class RefundJson extends JsonBase {
 
     private final String refundId;
     private final String paymentId;
@@ -34,18 +38,15 @@ public class RefundJson {
     private final DateTime requestedDate;
     private final DateTime effectiveDate;
 
-    public RefundJson(final Refund input) {
-        this(input.getId().toString(), input.getPaymentId().toString(), input.getRefundAmount(), input.isAdjusted(),
-             input.getEffectiveDate(), input.getEffectiveDate());
-    }
-
     @JsonCreator
     public RefundJson(@JsonProperty("refund_id") final String refundId,
                       @JsonProperty("paymentId") final String paymentId,
                       @JsonProperty("refundAmount") final BigDecimal refundAmount,
                       @JsonProperty("adjusted") final Boolean isAdjusted,
                       @JsonProperty("requestedDate") final DateTime requestedDate,
-                      @JsonProperty("effectiveDate") final DateTime effectiveDate) {
+                      @JsonProperty("effectiveDate") final DateTime effectiveDate,
+                      @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
+        super(auditLogs);
         this.refundId = refundId;
         this.paymentId = paymentId;
         this.refundAmount = refundAmount;
@@ -54,6 +55,15 @@ public class RefundJson {
         this.effectiveDate = effectiveDate;
     }
 
+    public RefundJson(final Refund refund) {
+        this(refund, null);
+    }
+
+    public RefundJson(final Refund refund, final List<AuditLog> auditLogs) {
+        this(refund.getId().toString(), refund.getPaymentId().toString(), refund.getRefundAmount(), refund.isAdjusted(),
+             refund.getEffectiveDate(), refund.getEffectiveDate(), toAuditLogJson(auditLogs));
+    }
+
     public String getRefundId() {
         return refundId;
     }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
index d803d10..cf1e2d0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/ExceptionMapperBase.java
@@ -29,6 +29,8 @@ public abstract class ExceptionMapperBase {
     private static final Logger log = LoggerFactory.getLogger(ExceptionMapperBase.class);
 
     protected Response buildConflictingRequestResponse(final Exception e, final UriInfo uriInfo) {
+        // Log the full stacktrace
+        log.warn("Conflicting request", e);
         return buildConflictingRequestResponse(e.toString(), uriInfo);
     }
 
@@ -41,6 +43,8 @@ public abstract class ExceptionMapperBase {
     }
 
     protected Response buildNotFoundResponse(final Exception e, final UriInfo uriInfo) {
+        // Log the full stacktrace
+        log.warn("Not found", e);
         return buildNotFoundResponse(e.toString(), uriInfo);
     }
 
@@ -53,6 +57,8 @@ public abstract class ExceptionMapperBase {
     }
 
     protected Response buildBadRequestResponse(final Exception e, final UriInfo uriInfo) {
+        // Log the full stacktrace
+        log.warn("Bad request", e);
         return buildBadRequestResponse(e.toString(), uriInfo);
     }
 
@@ -65,6 +71,8 @@ public abstract class ExceptionMapperBase {
     }
 
     protected Response buildInternalErrorResponse(final Exception e, final UriInfo uriInfo) {
+        // Log the full stacktrace
+        log.warn("Internal error", e);
         return buildInternalErrorResponse(e.toString(), uriInfo);
     }
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
index e8e3c61..ebbdcab 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/mappers/RuntimeExceptionMapper.java
@@ -24,12 +24,17 @@ import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 @Singleton
 @Provider
 public class RuntimeExceptionMapper extends ExceptionMapperBase implements ExceptionMapper<RuntimeException> {
 
     private final UriInfo uriInfo;
 
+    private static final Logger log = LoggerFactory.getLogger(RuntimeExceptionMapper.class);
+
     public RuntimeExceptionMapper(@Context final UriInfo uriInfo) {
         this.uriInfo = uriInfo;
     }
@@ -38,6 +43,8 @@ public class RuntimeExceptionMapper extends ExceptionMapperBase implements Excep
     public Response toResponse(final RuntimeException exception) {
         if (exception instanceof NullPointerException) {
             // Assume bad payload
+            exception.printStackTrace();
+            log.warn("Exception : " + exception.getMessage());
             return buildBadRequestResponse(exception, uriInfo);
         } else if (exception instanceof WebApplicationException) {
             // e.g. com.sun.jersey.api.NotFoundException
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index 20d0320..ff6a5fe 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -18,8 +18,10 @@ package com.ning.billing.jaxrs.resources;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import javax.ws.rs.Consumes;
@@ -50,6 +52,7 @@ import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.invoice.api.InvoicePayment;
 import com.ning.billing.invoice.api.InvoicePaymentApi;
 import com.ning.billing.invoice.api.InvoiceUserApi;
@@ -69,10 +72,12 @@ import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentApiException;
 import com.ning.billing.payment.api.PaymentMethod;
 import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagDefinitionApiException;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AuditLog;
 import com.ning.billing.util.dao.ObjectType;
 
 import com.google.common.base.Function;
@@ -107,9 +112,10 @@ public class AccountResource extends JaxRsResourceBase {
                            final PaymentApi paymentApi,
                            final EntitlementTimelineApi timelineApi,
                            final TagUserApi tagUserApi,
+                           final AuditUserApi auditUserApi,
                            final CustomFieldUserApi customFieldUserApi,
                            final Context context) {
-        super(uriBuilder, tagUserApi, customFieldUserApi);
+        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi);
         this.accountApi = accountApi;
         this.entitlementApi = entitlementApi;
         this.invoiceApi = invoiceApi;
@@ -133,7 +139,7 @@ public class AccountResource extends JaxRsResourceBase {
     @Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES)
     @Produces(APPLICATION_JSON)
     public Response getAccountBundles(@PathParam("accountId") final String accountId, @QueryParam(QUERY_EXTERNAL_KEY) final String externalKey)
-        throws AccountApiException, EntitlementUserApiException {
+            throws AccountApiException, EntitlementUserApiException {
 
         final UUID uuid = UUID.fromString(accountId);
         accountApi.getAccountById(uuid);
@@ -210,50 +216,77 @@ public class AccountResource extends JaxRsResourceBase {
     @GET
     @Path("/{accountId:" + UUID_PATTERN + "}/" + TIMELINE)
     @Produces(APPLICATION_JSON)
-    public Response getAccountTimeline(@PathParam("accountId") final String accountIdString) throws AccountApiException, PaymentApiException, EntitlementRepairException {
+    public Response getAccountTimeline(@PathParam("accountId") final String accountIdString,
+                                       @QueryParam("audit") @DefaultValue("false") final Boolean withAudit) throws AccountApiException, PaymentApiException, EntitlementRepairException {
         final UUID accountId = UUID.fromString(accountIdString);
         final Account account = accountApi.getAccountById(accountId);
 
         // Get the invoices
         final List<Invoice> invoices = invoiceApi.getInvoicesByAccount(account.getId());
+        final Map<UUID, List<AuditLog>> invoiceAuditLogs = new HashMap<UUID, List<AuditLog>>();
+        final Map<UUID, List<AuditLog>> invoiceItemsAuditLogs = new HashMap<UUID, List<AuditLog>>();
+        if (withAudit) {
+            for (final Invoice invoice : invoices) {
+                invoiceAuditLogs.put(invoice.getId(), auditUserApi.getAuditLogs(invoice.getId(), ObjectType.INVOICE));
+                for (final InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
+                    invoiceItemsAuditLogs.put(invoiceItem.getId(), auditUserApi.getAuditLogs(invoiceItem.getId(), ObjectType.INVOICE_ITEM));
+                }
+            }
+        }
 
         // Get the payments
         final List<Payment> payments = paymentApi.getAccountPayments(accountId);
+        final Map<UUID, List<AuditLog>> paymentsAuditLogs = new HashMap<UUID, List<AuditLog>>();
+        if (withAudit) {
+            for (final Payment payment : payments) {
+                paymentsAuditLogs.put(payment.getId(), auditUserApi.getAuditLogs(payment.getId(), ObjectType.PAYMENT));
+            }
+        }
 
         // Get the refunds
         final List<Refund> refunds = paymentApi.getAccountRefunds(account);
+        final Map<UUID, List<AuditLog>> refundsAuditLogs = new HashMap<UUID, List<AuditLog>>();
         final Multimap<UUID, Refund> refundsByPayment = ArrayListMultimap.<UUID, Refund>create();
         for (final Refund refund : refunds) {
+            if (withAudit) {
+                refundsAuditLogs.put(refund.getId(), auditUserApi.getAuditLogs(refund.getId(), ObjectType.REFUND));
+            }
             refundsByPayment.put(refund.getPaymentId(), refund);
         }
 
         // Get the chargebacks
         final List<InvoicePayment> chargebacks = invoicePaymentApi.getChargebacksByAccountId(accountId);
+        final Map<UUID, List<AuditLog>> chargebacksAuditLogs = new HashMap<UUID, List<AuditLog>>();
         final Multimap<UUID, InvoicePayment> chargebacksByPayment = ArrayListMultimap.<UUID, InvoicePayment>create();
         for (final InvoicePayment chargeback : chargebacks) {
+            if (withAudit) {
+                chargebacksAuditLogs.put(chargeback.getId(), auditUserApi.getAuditLogs(chargeback.getId(), ObjectType.INVOICE_PAYMENT));
+            }
             chargebacksByPayment.put(chargeback.getPaymentId(), chargeback);
         }
 
         // Get the bundles
         final List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(account.getId());
+        final Map<UUID, List<AuditLog>> bundlesAuditLogs = new HashMap<UUID, List<AuditLog>>();
         final List<BundleTimeline> bundlesTimeline = new LinkedList<BundleTimeline>();
-        for (final SubscriptionBundle cur : bundles) {
-            bundlesTimeline.add(timelineApi.getBundleTimeline(cur.getId()));
+        for (final SubscriptionBundle bundle : bundles) {
+            if (withAudit) {
+                bundlesAuditLogs.put(bundle.getId(), auditUserApi.getAuditLogs(bundle.getId(), ObjectType.BUNDLE));
+            }
+            bundlesTimeline.add(timelineApi.getBundleTimeline(bundle.getId()));
         }
 
         final AccountTimelineJson json = new AccountTimelineJson(account, invoices, payments, bundlesTimeline,
-                                                                 refundsByPayment, chargebacksByPayment);
+                                                                 refundsByPayment, chargebacksByPayment, invoiceAuditLogs,
+                                                                 invoiceItemsAuditLogs, paymentsAuditLogs, refundsAuditLogs,
+                                                                 chargebacksAuditLogs, bundlesAuditLogs);
 
         return Response.status(Status.OK).entity(json).build();
     }
 
-
-
-
-
     /*
-     * ************************** EMAIL NOTIFICATIONS FOR INVOICES ********************************
-     */
+    * ************************** EMAIL NOTIFICATIONS FOR INVOICES ********************************
+    */
 
     @GET
     @Path("/{accountId:" + UUID_PATTERN + "}/" + EMAIL_NOTIFICATIONS)
@@ -296,8 +329,8 @@ public class AccountResource extends JaxRsResourceBase {
                                 @QueryParam(QUERY_PAYMENT_NAME_ON_CC) final String nameOnCC) throws PaymentApiException {
         final List<Payment> payments = paymentApi.getAccountPayments(UUID.fromString(accountId));
         final List<PaymentJsonSimple> result = new ArrayList<PaymentJsonSimple>(payments.size());
-        for (final Payment cur : payments) {
-            result.add(new PaymentJsonSimple(cur));
+        for (final Payment payment : payments) {
+            result.add(new PaymentJsonSimple(payment));
         }
         return Response.status(Status.OK).entity(result).build();
     }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
index b1c3586..083a9a8 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/BundleResource.java
@@ -22,9 +22,11 @@ import java.util.UUID;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -33,6 +35,10 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
+import org.joda.time.DateTime;
+
+import com.ning.billing.entitlement.api.transfer.EntitlementTransferApi;
+import com.ning.billing.entitlement.api.transfer.EntitlementTransferApiException;
 import com.ning.billing.entitlement.api.user.EntitlementUserApi;
 import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
 import com.ning.billing.entitlement.api.user.Subscription;
@@ -62,18 +68,21 @@ public class BundleResource extends JaxRsResourceBase {
     private static final String TAG_URI = JaxrsResource.TAGS;
 
     private final EntitlementUserApi entitlementApi;
+    private final EntitlementTransferApi transferApi;
     private final Context context;
     private final JaxrsUriBuilder uriBuilder;
 
     @Inject
     public BundleResource(final JaxrsUriBuilder uriBuilder,
                           final EntitlementUserApi entitlementApi,
+                          final EntitlementTransferApi transferApi,
                           final TagUserApi tagUserApi,
                           final CustomFieldUserApi customFieldUserApi,
                           final Context context) {
         super(uriBuilder, tagUserApi, customFieldUserApi);
         this.uriBuilder = uriBuilder;
         this.entitlementApi = entitlementApi;
+        this.transferApi = transferApi;
         this.context = context;
     }
 
@@ -159,6 +168,27 @@ public class BundleResource extends JaxRsResourceBase {
         return super.getTags(UUID.fromString(id));
     }
 
+    @PUT
+    @Path("/{bundleId:" + UUID_PATTERN + "}")
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    public Response transferBundle(@PathParam(ID_PARAM_NAME) final String id,
+            @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+            @QueryParam(QUERY_BUNDLE_TRANSFER_ADDON) @DefaultValue("true") final Boolean transferAddOn,
+            final BundleJsonNoSubscriptions json,
+            @HeaderParam(HDR_CREATED_BY) final String createdBy,
+            @HeaderParam(HDR_REASON) final String reason,
+            @HeaderParam(HDR_COMMENT) final String comment,
+            @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementUserApiException, EntitlementTransferApiException {
+
+        final SubscriptionBundle bundle = entitlementApi.getBundleFromId(UUID.fromString(id));
+        final DateTime inputDate = (requestedDate != null) ? DATE_TIME_FORMATTER.parseDateTime(requestedDate) : null;
+        final SubscriptionBundle newBundle = transferApi.transferBundle(bundle.getAccountId(), UUID.fromString(json.getAccountId()), bundle.getKey(), inputDate, transferAddOn,
+                context.createContext(createdBy, reason, comment));
+
+        return uriBuilder.buildResponse(BundleResource.class, "getBundle", newBundle.getId(), uriInfo.getBaseUri().toString());
+    }
+
     @POST
     @Path("/{bundleId:" + UUID_PATTERN + "}/" + TAG_URI)
     @Consumes(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
index f4adea8..90be987 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
@@ -114,8 +114,8 @@ public class InvoiceResource extends JaxRsResourceBase {
 
         final List<Invoice> invoices = invoiceApi.getInvoicesByAccount(UUID.fromString(accountId));
         final List<InvoiceJsonSimple> result = new LinkedList<InvoiceJsonSimple>();
-        for (final Invoice cur : invoices) {
-            result.add(new InvoiceJsonSimple(cur));
+        for (final Invoice invoice : invoices) {
+            result.add(new InvoiceJsonSimple(invoice));
         }
 
         return Response.status(Status.OK).entity(result).build();
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 932d1e2..4c9ceb0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -61,6 +61,7 @@ public interface JaxrsResource {
     public static final String QUERY_PAYMENT_METHOD_PLUGIN_INFO = "withPluginInfo";
     public static final String QUERY_PAYMENT_METHOD_IS_DEFAULT = "isDefault";
 
+    public static final String QUERY_BUNDLE_TRANSFER_ADDON = "transferAddOn";
 
     public static final String ACCOUNTS = "accounts";
     public static final String ACCOUNTS_PATH = PREFIX + "/" + ACCOUNTS;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
index e092c39..6f2d2f1 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -26,11 +26,14 @@ import java.util.UUID;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.ning.billing.jaxrs.json.CustomFieldJson;
 import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
+import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagDefinitionApiException;
@@ -53,15 +56,26 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
     protected final JaxrsUriBuilder uriBuilder;
     protected final TagUserApi tagUserApi;
     protected final CustomFieldUserApi customFieldUserApi;
+    protected final AuditUserApi auditUserApi;
 
-    protected abstract ObjectType getObjectType();
+    protected final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime();
 
     public JaxRsResourceBase(final JaxrsUriBuilder uriBuilder,
                              final TagUserApi tagUserApi,
-                             final CustomFieldUserApi customFieldUserApi) {
+                             final CustomFieldUserApi customFieldUserApi,
+                             final AuditUserApi auditUserApi) {
         this.uriBuilder = uriBuilder;
         this.tagUserApi = tagUserApi;
         this.customFieldUserApi = customFieldUserApi;
+        this.auditUserApi = auditUserApi;
+    }
+
+    protected abstract ObjectType getObjectType();
+
+    public JaxRsResourceBase(final JaxrsUriBuilder uriBuilder,
+                             final TagUserApi tagUserApi,
+                             final CustomFieldUserApi customFieldUserApi) {
+        this(uriBuilder, tagUserApi, customFieldUserApi, null);
     }
 
     protected Response getTags(final UUID id) throws TagDefinitionApiException {
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
index 38027ff..03c30d2 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/SubscriptionResource.java
@@ -37,8 +37,6 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 
 import org.joda.time.DateTime;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -79,8 +77,6 @@ public class SubscriptionResource extends JaxRsResourceBase {
     private static final String CUSTOM_FIELD_URI = JaxrsResource.CUSTOM_FIELDS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
     private static final String TAG_URI = JaxrsResource.TAGS + "/{" + ID_PARAM_NAME + ":" + UUID_PATTERN + "}";
 
-    private final DateTimeFormatter DATE_TIME_FORMATTER = ISODateTimeFormat.dateTime();
-
     private final EntitlementUserApi entitlementApi;
     private final Context context;
     private final JaxrsUriBuilder uriBuilder;
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
index 51e2ac7..9018ab0 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/JaxrsTestSuite.java
@@ -16,7 +16,37 @@
 
 package com.ning.billing.jaxrs;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
 import com.ning.billing.KillbillTestSuite;
+import com.ning.billing.jaxrs.json.AuditLogJson;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
 
 public abstract class JaxrsTestSuite extends KillbillTestSuite {
+
+    private final Clock clock = new ClockMock();
+
+    protected static final ObjectMapper mapper = new ObjectMapper();
+
+    static {
+        mapper.registerModule(new JodaModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+    }
+
+    protected List<AuditLogJson> createAuditLogsJson() {
+        final List<AuditLogJson> auditLogs = new ArrayList<AuditLogJson>();
+        for (int i = 0; i < 20; i++) {
+            auditLogs.add(new AuditLogJson(UUID.randomUUID().toString(), clock.getUTCNow(), UUID.randomUUID().toString(),
+                                           UUID.randomUUID().toString(), UUID.randomUUID().toString(), UUID.randomUUID().toString()));
+        }
+
+        return auditLogs;
+    }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java
new file mode 100644
index 0000000..3c7e31c
--- /dev/null
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestAuditLogJson.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.json;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.jaxrs.JaxrsTestSuite;
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.DefaultAuditLog;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.clock.DefaultClock;
+import com.ning.billing.util.dao.EntityAudit;
+import com.ning.billing.util.dao.TableName;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+
+public class TestAuditLogJson extends JaxrsTestSuite {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private final Clock clock = new DefaultClock();
+
+    static {
+        mapper.registerModule(new JodaModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+    }
+
+    @Test(groups = "fast")
+    public void testJson() throws Exception {
+        final String changeType = UUID.randomUUID().toString();
+        final DateTime changeDate = clock.getUTCNow();
+        final String changedBy = UUID.randomUUID().toString();
+        final String reasonCode = UUID.randomUUID().toString();
+        final String comments = UUID.randomUUID().toString();
+        final String userToken = UUID.randomUUID().toString();
+
+        final AuditLogJson auditLogJson = new AuditLogJson(changeType, changeDate, changedBy, reasonCode, comments, userToken);
+        Assert.assertEquals(auditLogJson.getChangeType(), changeType);
+        Assert.assertEquals(auditLogJson.getChangeDate(), changeDate);
+        Assert.assertEquals(auditLogJson.getChangedBy(), changedBy);
+        Assert.assertEquals(auditLogJson.getReasonCode(), reasonCode);
+        Assert.assertEquals(auditLogJson.getComments(), comments);
+        Assert.assertEquals(auditLogJson.getUserToken(), userToken);
+
+        final String asJson = mapper.writeValueAsString(auditLogJson);
+        Assert.assertEquals(asJson, "{\"changeType\":\"" + auditLogJson.getChangeType() + "\"," +
+                                    "\"changeDate\":\"" + auditLogJson.getChangeDate().toDateTimeISO().toString() + "\"," +
+                                    "\"changedBy\":\"" + auditLogJson.getChangedBy() + "\"," +
+                                    "\"reasonCode\":\"" + auditLogJson.getReasonCode() + "\"," +
+                                    "\"comments\":\"" + auditLogJson.getComments() + "\"," +
+                                    "\"userToken\":\"" + auditLogJson.getUserToken() + "\"}");
+
+        final AuditLogJson fromJson = mapper.readValue(asJson, AuditLogJson.class);
+        Assert.assertEquals(fromJson, auditLogJson);
+    }
+
+    @Test(groups = "fast")
+    public void testConstructor() throws Exception {
+        final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+        final long recordId = Long.MAX_VALUE;
+        final ChangeType changeType = ChangeType.DELETE;
+        final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+        final String userName = UUID.randomUUID().toString();
+        final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+        final UserType userType = UserType.CUSTOMER;
+        final UUID userToken = UUID.randomUUID();
+        final ClockMock clock = new ClockMock();
+        final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+        final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+
+        final AuditLogJson auditLogJson = new AuditLogJson(auditLog);
+        Assert.assertEquals(auditLogJson.getChangeType(), changeType.toString());
+        Assert.assertNotNull(auditLogJson.getChangeDate());
+        Assert.assertEquals(auditLogJson.getChangedBy(), userName);
+        Assert.assertNull(auditLogJson.getReasonCode());
+        Assert.assertNull(auditLogJson.getComments());
+        Assert.assertEquals(auditLogJson.getUserToken(), userToken.toString());
+    }
+}
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
index 97caed0..d87274a 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonNoSubscriptions.java
@@ -16,34 +16,31 @@
 
 package com.ning.billing.jaxrs.json;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.jaxrs.JaxrsTestSuite;
 
 public class TestBundleJsonNoSubscriptions extends JaxrsTestSuite {
-    private static final ObjectMapper mapper = new ObjectMapper();
 
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final String bundleId = UUID.randomUUID().toString();
         final String accountId = UUID.randomUUID().toString();
         final String externalKey = UUID.randomUUID().toString();
-        final BundleJsonNoSubscriptions bundleJsonNoSubscriptions = new BundleJsonNoSubscriptions(bundleId, accountId, externalKey, null);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final BundleJsonNoSubscriptions bundleJsonNoSubscriptions = new BundleJsonNoSubscriptions(bundleId, accountId, externalKey, null, auditLogs);
         Assert.assertEquals(bundleJsonNoSubscriptions.getBundleId(), bundleId);
         Assert.assertEquals(bundleJsonNoSubscriptions.getAccountId(), accountId);
         Assert.assertEquals(bundleJsonNoSubscriptions.getExternalKey(), externalKey);
+        Assert.assertEquals(bundleJsonNoSubscriptions.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(bundleJsonNoSubscriptions);
-        Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonNoSubscriptions.getBundleId() + "\"," +
-                "\"accountId\":\"" + bundleJsonNoSubscriptions.getAccountId() + "\"," +
-                "\"externalKey\":\"" + bundleJsonNoSubscriptions.getExternalKey() + "\"}");
-
         final BundleJsonNoSubscriptions fromJson = mapper.readValue(asJson, BundleJsonNoSubscriptions.class);
         Assert.assertEquals(fromJson, bundleJsonNoSubscriptions);
     }
@@ -62,5 +59,6 @@ public class TestBundleJsonNoSubscriptions extends JaxrsTestSuite {
         Assert.assertEquals(bundleJsonNoSubscriptions.getBundleId(), bundleId.toString());
         Assert.assertEquals(bundleJsonNoSubscriptions.getExternalKey(), externalKey);
         Assert.assertEquals(bundleJsonNoSubscriptions.getAccountId(), accountId.toString());
+        Assert.assertNull(bundleJsonNoSubscriptions.getAuditLogs());
     }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
index e00ed56..62e56d3 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonSimple.java
@@ -16,29 +16,27 @@
 
 package com.ning.billing.jaxrs.json;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ning.billing.jaxrs.JaxrsTestSuite;
 
 public class TestBundleJsonSimple extends JaxrsTestSuite {
-    private static final ObjectMapper mapper = new ObjectMapper();
 
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final String bundleId = UUID.randomUUID().toString();
         final String externalKey = UUID.randomUUID().toString();
-        final BundleJsonSimple bundleJsonSimple = new BundleJsonSimple(bundleId, externalKey);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final BundleJsonSimple bundleJsonSimple = new BundleJsonSimple(bundleId, externalKey, auditLogs);
         Assert.assertEquals(bundleJsonSimple.getBundleId(), bundleId);
         Assert.assertEquals(bundleJsonSimple.getExternalKey(), externalKey);
+        Assert.assertEquals(bundleJsonSimple.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(bundleJsonSimple);
-        Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonSimple.getBundleId() + "\"," +
-                "\"externalKey\":\"" + bundleJsonSimple.getExternalKey() + "\"}");
-
         final BundleJsonSimple fromJson = mapper.readValue(asJson, BundleJsonSimple.class);
         Assert.assertEquals(fromJson, bundleJsonSimple);
     }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
index 84cac3c..36e328b 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleJsonWithSubscriptions.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.jaxrs.json;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -24,10 +25,6 @@ import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import com.google.common.collect.ImmutableList;
 import com.ning.billing.catalog.api.BillingPeriod;
 import com.ning.billing.catalog.api.PhaseType;
 import com.ning.billing.catalog.api.PlanPhaseSpecifier;
@@ -35,17 +32,12 @@ import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.entitlement.api.SubscriptionTransitionType;
 import com.ning.billing.entitlement.api.timeline.BundleTimeline;
 import com.ning.billing.entitlement.api.timeline.SubscriptionTimeline;
-import com.ning.billing.entitlement.api.user.SubscriptionBundle;
 import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.DefaultClock;
 
-public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
-    private static final ObjectMapper mapper = new ObjectMapper();
+import com.google.common.collect.ImmutableList;
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
+public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
 
     @Test(groups = "fast")
     public void testJson() throws Exception {
@@ -67,24 +59,14 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
         final UUID bundleId = UUID.randomUUID();
         final String externalKey = UUID.randomUUID().toString();
         final SubscriptionJsonWithEvents subscription = new SubscriptionJsonWithEvents(bundleId, subscriptionTimeline);
-        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription));
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription), auditLogs);
         Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
         Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
         Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 1);
+        Assert.assertEquals(bundleJsonWithSubscriptions.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(bundleJsonWithSubscriptions);
-        Assert.assertEquals(asJson, "{\"bundleId\":\"" + bundleJsonWithSubscriptions.getBundleId() + "\"," +
-                "\"externalKey\":\"" + bundleJsonWithSubscriptions.getExternalKey() + "\"," +
-                "\"subscriptions\":[{\"events\":[{\"eventId\":\"" + event.getEventId().toString() + "\"," +
-                "\"billingPeriod\":\"" + event.getPlanPhaseSpecifier().getBillingPeriod().toString() + "\"," +
-                "\"product\":\"" + event.getPlanPhaseSpecifier().getProductName() + "\"," +
-                "\"priceList\":\"" + event.getPlanPhaseSpecifier().getPriceListName() + "\"," +
-                "\"eventType\":\"" + event.getSubscriptionTransitionType().toString() + "\"," +
-                "\"phase\":\"" + event.getPlanPhaseSpecifier().getPhaseType() + "\"," +
-                "\"requestedDate\":null," +
-                "\"effectiveDate\":\"" + event.getEffectiveDate().toDateTimeISO().toString() + "\"}]," +
-                "\"subscriptionId\":\"" + subscriptionTimeline.getId().toString() + "\",\"deletedEvents\":null,\"newEvents\":null}]}");
-
         final BundleJsonWithSubscriptions fromJson = mapper.readValue(asJson, BundleJsonWithSubscriptions.class);
         Assert.assertEquals(fromJson, bundleJsonWithSubscriptions);
     }
@@ -113,7 +95,7 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
         Mockito.when(bundleTimeline.getExternalKey()).thenReturn(externalKey);
         Mockito.when(bundleTimeline.getSubscriptions()).thenReturn(ImmutableList.<SubscriptionTimeline>of(subscriptionTimeline));
 
-        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundleTimeline);
+        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundleTimeline, null);
         Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
         Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
         Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 1);
@@ -124,19 +106,21 @@ public class TestBundleJsonWithSubscriptions extends JaxrsTestSuite {
         // Note - ms are truncated
         Assert.assertEquals(events.getEvents().get(0).getEffectiveDate(), DefaultClock.toUTCDateTime(effectiveDate));
         Assert.assertEquals(events.getEvents().get(0).getEventId(), eventId.toString());
+        Assert.assertNull(bundleJsonWithSubscriptions.getAuditLogs());
     }
 
     @Test(groups = "fast")
     public void testFromSubscriptionBundle() throws Exception {
-        final SubscriptionBundle bundle = Mockito.mock(SubscriptionBundle.class);
+        final BundleTimeline bundle = Mockito.mock(BundleTimeline.class);
         final UUID bundleId = UUID.randomUUID();
         final String externalKey = UUID.randomUUID().toString();
-        Mockito.when(bundle.getId()).thenReturn(bundleId);
-        Mockito.when(bundle.getKey()).thenReturn(externalKey);
+        Mockito.when(bundle.getBundleId()).thenReturn(bundleId);
+        Mockito.when(bundle.getExternalKey()).thenReturn(externalKey);
 
-        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(bundle);
+        final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = new BundleJsonWithSubscriptions(null, bundle, null);
         Assert.assertEquals(bundleJsonWithSubscriptions.getBundleId(), bundleId.toString());
         Assert.assertEquals(bundleJsonWithSubscriptions.getExternalKey(), externalKey);
-        Assert.assertNull(bundleJsonWithSubscriptions.getSubscriptions());
+        Assert.assertEquals(bundleJsonWithSubscriptions.getSubscriptions().size(), 0);
+        Assert.assertNull(bundleJsonWithSubscriptions.getAuditLogs());
     }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
index c798b24..17df65d 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestBundleTimelineJson.java
@@ -35,22 +35,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
 
 public class TestBundleTimelineJson extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final String viewId = UUID.randomUUID().toString();
@@ -59,63 +49,15 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
         final BundleJsonWithSubscriptions bundleJsonWithSubscriptions = createBundleWithSubscriptions();
         final InvoiceJsonSimple invoiceJsonSimple = createInvoice();
         final PaymentJsonSimple paymentJsonSimple = createPayment(UUID.fromString(invoiceJsonSimple.getAccountId()),
-                UUID.fromString(invoiceJsonSimple.getInvoiceId()));
+                                                                  UUID.fromString(invoiceJsonSimple.getInvoiceId()));
 
         final BundleTimelineJson bundleTimelineJson = new BundleTimelineJson(viewId,
-                bundleJsonWithSubscriptions,
-                ImmutableList.<PaymentJsonSimple>of(paymentJsonSimple),
-                ImmutableList.<InvoiceJsonSimple>of(invoiceJsonSimple),
-                reason);
+                                                                             bundleJsonWithSubscriptions,
+                                                                             ImmutableList.<PaymentJsonSimple>of(paymentJsonSimple),
+                                                                             ImmutableList.<InvoiceJsonSimple>of(invoiceJsonSimple),
+                                                                             reason);
 
         final String asJson = mapper.writeValueAsString(bundleTimelineJson);
-
-        final SubscriptionJsonWithEvents subscription = bundleTimelineJson.getBundle().getSubscriptions().get(0);
-        final SubscriptionJsonWithEvents.SubscriptionReadEventJson event = subscription.getEvents().get(0);
-        final PaymentJsonSimple payment = bundleTimelineJson.getPayments().get(0);
-        final InvoiceJsonSimple invoice = bundleTimelineJson.getInvoices().get(0);
-
-        Assert.assertEquals(asJson, "{\"viewId\":\"" + bundleTimelineJson.getViewId() + "\"," +
-            "\"bundle\":{\"bundleId\":\"" + bundleTimelineJson.getBundle().getBundleId() + "\"," +
-            "\"externalKey\":\"" + bundleTimelineJson.getBundle().getExternalKey() + "\"," +
-            "\"subscriptions\":" +
-            "[{\"events\":[{\"eventId\":\"" + event.getEventId() + "\"," +
-            "\"billingPeriod\":\"" + event.getBillingPeriod() + "\"," +
-            "\"product\":\"" + event.getProduct() + "\"," +
-            "\"priceList\":\"" + event.getPriceList() + "\"," +
-            "\"eventType\":\"" + event.getEventType() + "\"," +
-            "\"phase\":\"" + event.getPhase() + "\"," +
-            "\"requestedDate\":null," +
-            "\"effectiveDate\":\"" + event.getEffectiveDate().toDateTimeISO().toString() + "\"}]," +
-            "\"subscriptionId\":\"" + subscription.getSubscriptionId() + "\"," +
-            "\"deletedEvents\":null," +
-            "\"newEvents\":null}]}," +
-            "\"payments\":[{\"amount\":" + payment.getAmount() + "," +
-            "\"paidAmount\":" + payment.getPaidAmount() + "," +
-            "\"accountId\":\"" + payment.getAccountId() + "\"," +
-            "\"invoiceId\":\"" + payment.getInvoiceId() + "\"," +
-            "\"paymentId\":\"" + payment.getPaymentId() + "\"," +
-            "\"paymentMethodId\":\"" + payment.getPaymentMethodId() + "\"," +
-            "\"requestedDate\":\"" + payment.getRequestedDate().toDateTimeISO().toString() + "\"," +
-            "\"effectiveDate\":\"" + payment.getEffectiveDate().toDateTimeISO().toString() + "\"," +
-            "\"retryCount\":" + payment.getRetryCount() + "," +
-            "\"currency\":\"" + payment.getCurrency() + "\"," +
-            "\"status\":\"" + payment.getStatus() + "\"," +
-            "\"gatewayErrorCode\":\"" + payment.getGatewayErrorCode() + "\"," +
-            "\"gatewayErrorMsg\":\"" + payment.getGatewayErrorMsg() + "\"," +
-            "\"extFirstPaymentIdRef\":\"" + payment.getExtFirstPaymentIdRef() + "\"," +
-            "\"extSecondPaymentIdRef\":\"" + payment.getExtSecondPaymentIdRef() + "\"}]," +
-            "\"invoices\":[{\"amount\":" + invoice.getAmount() + "," +
-            "\"cba\":" + invoice.getCBA() + "," +
-            "\"creditAdj\":" + invoice.getCreditAdj() + "," +
-            "\"refundAdj\":" + invoice.getRefundAdj() + "," +
-            "\"invoiceId\":\"" + invoice.getInvoiceId() + "\"," +
-            "\"invoiceDate\":\"" + invoice.getInvoiceDate().toString() + "\"," +
-            "\"targetDate\":\"" + invoice.getTargetDate() + "\"," +
-            "\"invoiceNumber\":\"" + invoice.getInvoiceNumber() + "\"," +
-            "\"balance\":" + invoice.getBalance() + "," +
-            "\"accountId\":\"" + invoice.getAccountId() + "\"}]," +
-            "\"reasonForChange\":\"" + reason + "\"}");
-
         final BundleTimelineJson fromJson = mapper.readValue(asJson, BundleTimelineJson.class);
         Assert.assertEquals(fromJson, bundleTimelineJson);
     }
@@ -125,8 +67,8 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
         final DateTime effectiveDate = clock.getUTCNow();
         final UUID eventId = UUID.randomUUID();
         final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(UUID.randomUUID().toString(), ProductCategory.BASE,
-                BillingPeriod.NO_BILLING_PERIOD, UUID.randomUUID().toString(),
-                PhaseType.EVERGREEN);
+                                                                             BillingPeriod.NO_BILLING_PERIOD, UUID.randomUUID().toString(),
+                                                                             PhaseType.EVERGREEN);
         Mockito.when(event.getEffectiveDate()).thenReturn(effectiveDate);
         Mockito.when(event.getEventId()).thenReturn(eventId);
         Mockito.when(event.getSubscriptionTransitionType()).thenReturn(SubscriptionTransitionType.CREATE);
@@ -140,7 +82,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
         final String externalKey = UUID.randomUUID().toString();
         final SubscriptionJsonWithEvents subscription = new SubscriptionJsonWithEvents(bundleId, subscriptionTimeline);
 
-        return new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription));
+        return new BundleJsonWithSubscriptions(bundleId.toString(), externalKey, ImmutableList.<SubscriptionJsonWithEvents>of(subscription), null);
     }
 
     private InvoiceJsonSimple createInvoice() {
@@ -156,7 +98,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
         final BigDecimal balance = BigDecimal.ZERO;
 
         return new InvoiceJsonSimple(invoiceAmount, cba, creditAdj, refundAdj, invoiceId.toString(), invoiceDate,
-                targetDate, invoiceNumber, balance, accountId.toString());
+                                     targetDate, invoiceNumber, balance, accountId.toString(), null);
     }
 
     private PaymentJsonSimple createPayment(final UUID accountId, final UUID invoiceId) {
@@ -175,6 +117,7 @@ public class TestBundleTimelineJson extends JaxrsTestSuite {
         final String extSecondPaymentIdRef = UUID.randomUUID().toString();
 
         return new PaymentJsonSimple(amount, paidAmount, accountId.toString(), invoiceId.toString(), paymentId.toString(),
-                paymentMethodId.toString(), paymentRequestedDate, paymentEffectiveDate, retryCount, currency, status, gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef, extSecondPaymentIdRef);
+                                     paymentMethodId.toString(), paymentRequestedDate, paymentEffectiveDate, retryCount, currency, status,
+                                     gatewayErrorCode, gatewayErrorMsg, extFirstPaymentIdRef, extSecondPaymentIdRef, null);
     }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
index af4c8c6..d58c565 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackCollectionJson.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -24,13 +25,15 @@ import org.joda.time.DateTimeZone;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.ning.billing.jaxrs.JaxrsTestSuite;
+
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
-import com.ning.billing.jaxrs.JaxrsTestSuite;
 
 public class TestChargebackCollectionJson extends JaxrsTestSuite {
+
     private static final ObjectMapper mapper = new ObjectMapper();
 
     static {
@@ -45,22 +48,18 @@ public class TestChargebackCollectionJson extends JaxrsTestSuite {
         final BigDecimal chargebackAmount = BigDecimal.TEN;
         final String paymentId = UUID.randomUUID().toString();
         final String reason = UUID.randomUUID().toString();
-        final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId, reason);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId,
+                                                                 reason, auditLogs);
 
         final String accountId = UUID.randomUUID().toString();
         final ChargebackCollectionJson chargebackCollectionJson = new ChargebackCollectionJson(accountId, ImmutableList.<ChargebackJson>of(chargebackJson));
         Assert.assertEquals(chargebackCollectionJson.getAccountId(), accountId);
         Assert.assertEquals(chargebackCollectionJson.getChargebacks().size(), 1);
         Assert.assertEquals(chargebackCollectionJson.getChargebacks().get(0), chargebackJson);
+        Assert.assertEquals(chargebackCollectionJson.getChargebacks().get(0).getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(chargebackCollectionJson);
-        Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId + "\",\"chargebacks\":[" +
-                "{\"requestedDate\":\"" + chargebackJson.getRequestedDate() + "\"," +
-                "\"effectiveDate\":\"" + chargebackJson.getEffectiveDate() + "\"," +
-                "\"chargebackAmount\":" + chargebackJson.getChargebackAmount() + "," +
-                "\"paymentId\":\"" + chargebackJson.getPaymentId() + "\"," +
-                "\"reason\":\"" + chargebackJson.getReason() + "\"}]}");
-
         final ChargebackCollectionJson fromJson = mapper.readValue(asJson, ChargebackCollectionJson.class);
         Assert.assertEquals(fromJson, chargebackCollectionJson);
     }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
index 006582d..b6d0b5e 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestChargebackJson.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -44,20 +45,17 @@ public class TestChargebackJson extends JaxrsTestSuite {
         final BigDecimal chargebackAmount = BigDecimal.TEN;
         final String paymentId = UUID.randomUUID().toString();
         final String reason = UUID.randomUUID().toString();
-        final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId, reason);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final ChargebackJson chargebackJson = new ChargebackJson(requestedDate, effectiveDate, chargebackAmount, paymentId,
+                                                                 reason, auditLogs);
         Assert.assertEquals(chargebackJson.getRequestedDate(), requestedDate);
         Assert.assertEquals(chargebackJson.getEffectiveDate(), effectiveDate);
         Assert.assertEquals(chargebackJson.getChargebackAmount(), chargebackAmount);
         Assert.assertEquals(chargebackJson.getPaymentId(), paymentId);
         Assert.assertEquals(chargebackJson.getReason(), reason);
+        Assert.assertEquals(chargebackJson.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(chargebackJson);
-        Assert.assertEquals(asJson, "{\"requestedDate\":\"" + chargebackJson.getRequestedDate() + "\"," +
-                "\"effectiveDate\":\"" + chargebackJson.getEffectiveDate() + "\"," +
-                "\"chargebackAmount\":" + chargebackJson.getChargebackAmount() + "," +
-                "\"paymentId\":\"" + chargebackJson.getPaymentId() + "\"," +
-                "\"reason\":\"" + chargebackJson.getReason() + "\"}");
-
         final ChargebackJson fromJson = mapper.readValue(asJson, ChargebackJson.class);
         Assert.assertEquals(fromJson, chargebackJson);
     }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
index 0579cac..04e8177 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditCollectionJson.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -27,22 +28,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
 
 public class TestCreditCollectionJson extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final UUID accountId = UUID.randomUUID();
@@ -53,23 +44,17 @@ public class TestCreditCollectionJson extends JaxrsTestSuite {
         final DateTime requestedDate = clock.getUTCNow();
         final DateTime effectiveDate = clock.getUTCNow();
         final String reason = UUID.randomUUID().toString();
-        final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate,
+                                                     effectiveDate, reason, accountId, auditLogs);
 
         final CreditCollectionJson creditCollectionJson = new CreditCollectionJson(accountId, ImmutableList.<CreditJson>of(creditJson));
         Assert.assertEquals(creditCollectionJson.getAccountId(), accountId);
         Assert.assertEquals(creditCollectionJson.getCredits().size(), 1);
         Assert.assertEquals(creditCollectionJson.getCredits().get(0), creditJson);
+        Assert.assertEquals(creditCollectionJson.getCredits().get(0).getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(creditCollectionJson);
-        Assert.assertEquals(asJson, "{\"accountId\":\"" + accountId.toString() + "\"," +
-                                    "\"credits\":[{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
-                                    "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
-                                    "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
-                                    "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
-                                    "\"reason\":\"" + creditJson.getReason() + "\"," +
-                                    "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}]}");
-
         final CreditCollectionJson fromJson = mapper.readValue(asJson, CreditCollectionJson.class);
         Assert.assertEquals(fromJson, creditCollectionJson);
     }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
index b18156c..913c5ec 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestCreditJson.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -27,21 +28,10 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-
 public class TestCreditJson extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final BigDecimal creditAmount = BigDecimal.TEN;
@@ -51,8 +41,9 @@ public class TestCreditJson extends JaxrsTestSuite {
         final DateTime effectiveDate = clock.getUTCNow();
         final String reason = UUID.randomUUID().toString();
         final UUID accountId = UUID.randomUUID();
-
-        final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
+        final CreditJson creditJson = new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate,
+                                                     reason, accountId, auditLogs);
         Assert.assertEquals(creditJson.getRequestedDate(), requestedDate);
         Assert.assertEquals(creditJson.getEffectiveDate(), effectiveDate);
         Assert.assertEquals(creditJson.getCreditAmount(), creditAmount);
@@ -62,14 +53,6 @@ public class TestCreditJson extends JaxrsTestSuite {
         Assert.assertEquals(creditJson.getAccountId(), accountId);
 
         final String asJson = mapper.writeValueAsString(creditJson);
-        Assert.assertEquals(asJson, "{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
-                                    "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
-                                    "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
-                                    "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
-                                    "\"reason\":\"" + creditJson.getReason() + "\"," +
-                                    "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}");
-
         final CreditJson fromJson = mapper.readValue(asJson, CreditJson.class);
         Assert.assertEquals(fromJson, creditJson);
     }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
index b517039..0e2ade2 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonSimple.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.LocalDate;
@@ -29,21 +30,10 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-
 public class TestInvoiceJsonSimple extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final BigDecimal amount = BigDecimal.TEN;
@@ -56,8 +46,9 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
         final String invoiceNumber = UUID.randomUUID().toString();
         final BigDecimal balance = BigDecimal.ZERO;
         final String accountId = UUID.randomUUID().toString();
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
         final InvoiceJsonSimple invoiceJsonSimple = new InvoiceJsonSimple(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
-                                                                          targetDate, invoiceNumber, balance, accountId);
+                                                                          targetDate, invoiceNumber, balance, accountId, auditLogs);
         Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
         Assert.assertEquals(invoiceJsonSimple.getCBA(), cba);
         Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
@@ -68,19 +59,9 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonSimple.getInvoiceNumber(), invoiceNumber);
         Assert.assertEquals(invoiceJsonSimple.getBalance(), balance);
         Assert.assertEquals(invoiceJsonSimple.getAccountId(), accountId);
+        Assert.assertEquals(invoiceJsonSimple.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(invoiceJsonSimple);
-        Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonSimple.getAmount().toString() + "," +
-                                    "\"cba\":" + invoiceJsonSimple.getCBA().toString() + "," +
-                                    "\"creditAdj\":" + invoiceJsonSimple.getCreditAdj().toString() + "," +
-                                    "\"refundAdj\":" + invoiceJsonSimple.getRefundAdj().toString() + "," +
-                                    "\"invoiceId\":\"" + invoiceJsonSimple.getInvoiceId() + "\"," +
-                                    "\"invoiceDate\":\"" + invoiceJsonSimple.getInvoiceDate().toString() + "\"," +
-                                    "\"targetDate\":\"" + invoiceJsonSimple.getTargetDate().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + invoiceJsonSimple.getInvoiceNumber() + "\"," +
-                                    "\"balance\":" + invoiceJsonSimple.getBalance().toString() + "," +
-                                    "\"accountId\":\"" + invoiceJsonSimple.getAccountId() + "\"}");
-
         final InvoiceJsonSimple fromJson = mapper.readValue(asJson, InvoiceJsonSimple.class);
         Assert.assertEquals(fromJson, invoiceJsonSimple);
     }
@@ -110,5 +91,6 @@ public class TestInvoiceJsonSimple extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonSimple.getInvoiceNumber(), String.valueOf(invoice.getInvoiceNumber()));
         Assert.assertEquals(invoiceJsonSimple.getBalance(), invoice.getBalance());
         Assert.assertEquals(invoiceJsonSimple.getAccountId(), invoice.getAccountId().toString());
+        Assert.assertNull(invoiceJsonSimple.getAuditLogs());
     }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
index 5c2ca0b..d312294 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithBundleKeys.java
@@ -31,22 +31,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
 
 public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final BigDecimal amount = BigDecimal.TEN;
@@ -60,11 +50,12 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
         final BigDecimal balance = BigDecimal.ZERO;
         final String accountId = UUID.randomUUID().toString();
         final String bundleKeys = UUID.randomUUID().toString();
-        CreditJson creditJson = createCreditJson();
+        final CreditJson creditJson = createCreditJson();
         final List<CreditJson> credits = ImmutableList.<CreditJson>of(creditJson);
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
         final InvoiceJsonWithBundleKeys invoiceJsonSimple = new InvoiceJsonWithBundleKeys(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
                                                                                           targetDate, invoiceNumber, balance, accountId, bundleKeys,
-                                                                                          credits);
+                                                                                          credits, auditLogs);
         Assert.assertEquals(invoiceJsonSimple.getAmount(), amount);
         Assert.assertEquals(invoiceJsonSimple.getCBA(), cba);
         Assert.assertEquals(invoiceJsonSimple.getCreditAdj(), creditAdj);
@@ -77,28 +68,9 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonSimple.getAccountId(), accountId);
         Assert.assertEquals(invoiceJsonSimple.getBundleKeys(), bundleKeys);
         Assert.assertEquals(invoiceJsonSimple.getCredits(), credits);
+        Assert.assertEquals(invoiceJsonSimple.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(invoiceJsonSimple);
-        Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonSimple.getAmount().toString() + "," +
-                                    "\"cba\":" + invoiceJsonSimple.getCBA().toString() + "," +
-                                    "\"creditAdj\":" + invoiceJsonSimple.getCreditAdj().toString() + "," +
-                                    "\"refundAdj\":" + invoiceJsonSimple.getRefundAdj().toString() + "," +
-                                    "\"invoiceId\":\"" + invoiceJsonSimple.getInvoiceId() + "\"," +
-                                    "\"invoiceDate\":\"" + invoiceJsonSimple.getInvoiceDate().toString() + "\"," +
-                                    "\"targetDate\":\"" + invoiceJsonSimple.getTargetDate().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + invoiceJsonSimple.getInvoiceNumber() + "\"," +
-                                    "\"balance\":" + invoiceJsonSimple.getBalance().toString() + "," +
-                                    "\"accountId\":\"" + invoiceJsonSimple.getAccountId() + "\"," +
-                                    "\"credits\":[" +
-                                    "{\"creditAmount\":" + creditJson.getCreditAmount() + "," +
-                                    "\"invoiceId\":\"" + creditJson.getInvoiceId().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + creditJson.getInvoiceNumber() + "\"," +
-                                    "\"requestedDate\":\"" + creditJson.getRequestedDate().toDateTimeISO().toString() + "\"," +
-                                    "\"effectiveDate\":\"" + creditJson.getEffectiveDate().toDateTimeISO().toString() + "\"," +
-                                    "\"reason\":\"" + creditJson.getReason() + "\"," +
-                                    "\"accountId\":\"" + creditJson.getAccountId().toString() + "\"}]," +
-                                    "\"bundleKeys\":\"" + invoiceJsonSimple.getBundleKeys() + "\"}");
-
         final InvoiceJsonWithBundleKeys fromJson = mapper.readValue(asJson, InvoiceJsonWithBundleKeys.class);
         Assert.assertEquals(fromJson, invoiceJsonSimple);
     }
@@ -120,7 +92,7 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
         final String bundleKeys = UUID.randomUUID().toString();
         final List<CreditJson> credits = ImmutableList.<CreditJson>of(createCreditJson());
 
-        final InvoiceJsonWithBundleKeys invoiceJsonWithBundleKeys = new InvoiceJsonWithBundleKeys(invoice, bundleKeys, credits);
+        final InvoiceJsonWithBundleKeys invoiceJsonWithBundleKeys = new InvoiceJsonWithBundleKeys(invoice, bundleKeys, credits, null);
         Assert.assertEquals(invoiceJsonWithBundleKeys.getAmount(), invoice.getChargedAmount());
         Assert.assertEquals(invoiceJsonWithBundleKeys.getCBA(), invoice.getCBAAmount());
         Assert.assertEquals(invoiceJsonWithBundleKeys.getCreditAdj(), invoice.getCreditAdjAmount());
@@ -133,6 +105,7 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonWithBundleKeys.getAccountId(), invoice.getAccountId().toString());
         Assert.assertEquals(invoiceJsonWithBundleKeys.getBundleKeys(), bundleKeys);
         Assert.assertEquals(invoiceJsonWithBundleKeys.getCredits(), credits);
+        Assert.assertNull(invoiceJsonWithBundleKeys.getAuditLogs());
     }
 
     private CreditJson createCreditJson() {
@@ -143,6 +116,6 @@ public class TestInvoiceJsonWithBundleKeys extends JaxrsTestSuite {
         final DateTime effectiveDate = clock.getUTCNow();
         final String reason = UUID.randomUUID().toString();
         final UUID accountId = UUID.randomUUID();
-        return new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId);
+        return new CreditJson(creditAmount, invoiceId, invoiceNumber, requestedDate, effectiveDate, reason, accountId, null);
     }
 }
diff --git a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
index e849f0e..c66bc4f 100644
--- a/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
+++ b/jaxrs/src/test/java/com/ning/billing/jaxrs/json/TestInvoiceJsonWithItems.java
@@ -17,6 +17,7 @@
 package com.ning.billing.jaxrs.json;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.LocalDate;
@@ -31,22 +32,12 @@ import com.ning.billing.jaxrs.JaxrsTestSuite;
 import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.DefaultClock;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
 import com.google.common.collect.ImmutableList;
 
 public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
-
     private final Clock clock = new DefaultClock();
 
-    static {
-        mapper.registerModule(new JodaModule());
-        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-    }
-
     @Test(groups = "fast")
     public void testJson() throws Exception {
         final BigDecimal amount = BigDecimal.TEN;
@@ -60,9 +51,10 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
         final BigDecimal balance = BigDecimal.ZERO;
         final String accountId = UUID.randomUUID().toString();
         final InvoiceItemJsonSimple invoiceItemJsonSimple = createInvoiceItemJson();
+        final List<AuditLogJson> auditLogs = createAuditLogsJson();
         final InvoiceJsonWithItems invoiceJsonWithItems = new InvoiceJsonWithItems(amount, cba, creditAdj, refundAdj, invoiceId, invoiceDate,
                                                                                    targetDate, invoiceNumber, balance, accountId,
-                                                                                   ImmutableList.<InvoiceItemJsonSimple>of(invoiceItemJsonSimple));
+                                                                                   ImmutableList.<InvoiceItemJsonSimple>of(invoiceItemJsonSimple), auditLogs);
         Assert.assertEquals(invoiceJsonWithItems.getAmount(), amount);
         Assert.assertEquals(invoiceJsonWithItems.getCBA(), cba);
         Assert.assertEquals(invoiceJsonWithItems.getCreditAdj(), creditAdj);
@@ -75,30 +67,9 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonWithItems.getAccountId(), accountId);
         Assert.assertEquals(invoiceJsonWithItems.getItems().size(), 1);
         Assert.assertEquals(invoiceJsonWithItems.getItems().get(0), invoiceItemJsonSimple);
+        Assert.assertEquals(invoiceJsonWithItems.getAuditLogs(), auditLogs);
 
         final String asJson = mapper.writeValueAsString(invoiceJsonWithItems);
-        Assert.assertEquals(asJson, "{\"amount\":" + invoiceJsonWithItems.getAmount().toString() + "," +
-                                    "\"cba\":" + invoiceJsonWithItems.getCBA().toString() + "," +
-                                    "\"creditAdj\":" + invoiceJsonWithItems.getCreditAdj().toString() + "," +
-                                    "\"refundAdj\":" + invoiceJsonWithItems.getRefundAdj().toString() + "," +
-                                    "\"invoiceId\":\"" + invoiceJsonWithItems.getInvoiceId() + "\"," +
-                                    "\"invoiceDate\":\"" + invoiceJsonWithItems.getInvoiceDate().toString() + "\"," +
-                                    "\"targetDate\":\"" + invoiceJsonWithItems.getTargetDate().toString() + "\"," +
-                                    "\"invoiceNumber\":\"" + invoiceJsonWithItems.getInvoiceNumber() + "\"," +
-                                    "\"balance\":" + invoiceJsonWithItems.getBalance().toString() + "," +
-                                    "\"accountId\":\"" + invoiceJsonWithItems.getAccountId() + "\"," +
-                                    "\"items\":[{\"invoiceId\":\"" + invoiceItemJsonSimple.getInvoiceId().toString() + "\"," +
-                                    "\"accountId\":\"" + invoiceItemJsonSimple.getAccountId().toString() + "\"," +
-                                    "\"bundleId\":\"" + invoiceItemJsonSimple.getBundleId().toString() + "\"," +
-                                    "\"subscriptionId\":\"" + invoiceItemJsonSimple.getSubscriptionId().toString() + "\"," +
-                                    "\"planName\":\"" + invoiceItemJsonSimple.getPlanName() + "\"," +
-                                    "\"phaseName\":\"" + invoiceItemJsonSimple.getPhaseName() + "\"," +
-                                    "\"description\":\"" + invoiceItemJsonSimple.getDescription() + "\"," +
-                                    "\"startDate\":\"" + invoiceItemJsonSimple.getStartDate().toString() + "\"," +
-                                    "\"endDate\":\"" + invoiceItemJsonSimple.getEndDate().toString() + "\"," +
-                                    "\"amount\":" + invoiceItemJsonSimple.getAmount().toString() + "," +
-                                    "\"currency\":\"" + invoiceItemJsonSimple.getCurrency().toString() + "\"}]}");
-
         final InvoiceJsonWithItems fromJson = mapper.readValue(asJson, InvoiceJsonWithItems.class);
         Assert.assertEquals(fromJson, invoiceJsonWithItems);
     }
@@ -125,6 +96,8 @@ public class TestInvoiceJsonWithItems extends JaxrsTestSuite {
         Assert.assertEquals(invoiceJsonWithItems.getBalance(), invoice.getBalance());
         Assert.assertEquals(invoiceJsonWithItems.getAccountId(), invoice.getAccountId().toString());
         Assert.assertEquals(invoiceJsonWithItems.getItems().size(), 1);
+        Assert.assertNull(invoiceJsonWithItems.getAuditLogs());
+
         final InvoiceItemJsonSimple invoiceItemJsonSimple = invoiceJsonWithItems.getItems().get(0);
         Assert.assertEquals(invoiceItemJsonSimple.getInvoiceId(), invoiceItem.getInvoiceId());
         Assert.assertEquals(invoiceItemJsonSimple.getAccountId(), invoiceItem.getAccountId());

pom.xml 16(+16 -0)

diff --git a/pom.xml b/pom.xml
index 7d00037..03f99e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
         <module>junction</module>
         <module>overdue</module>
         <module>payment</module>
+        <module>usage</module>
         <module>util</module>
         <module>jaxrs</module>
         <module>server</module>
@@ -177,6 +178,11 @@
             </dependency>
             <dependency>
                 <groupId>com.ning.billing</groupId>
+                <artifactId>killbill-usage</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.ning.billing</groupId>
                 <artifactId>killbill-overdue</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -216,6 +222,11 @@
                 <version>2.0.0</version>
             </dependency>
             <dependency>
+                <groupId>com.fasterxml.jackson.dataformat</groupId>
+                <artifactId>jackson-dataformat-smile</artifactId>
+                <version>2.0.1</version>
+            </dependency>
+            <dependency>
                 <groupId>com.fasterxml.jackson.datatype</groupId>
                 <artifactId>jackson-datatype-joda</artifactId>
                 <version>2.0.1</version>
@@ -226,6 +237,11 @@
                 <version>2.0.0</version>
             </dependency>
             <dependency>
+                <groupId>com.fasterxml.util</groupId>
+                <artifactId>low-gc-membuffers</artifactId>
+                <version>0.9.0</version>
+            </dependency>
+            <dependency>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
                 <version>12.0</version>
diff --git a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
index fda58c0..32ff962 100644
--- a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
+++ b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
@@ -41,6 +41,7 @@ import com.ning.billing.junction.glue.DefaultJunctionModule;
 import com.ning.billing.payment.glue.PaymentModule;
 import com.ning.billing.util.email.EmailModule;
 import com.ning.billing.util.email.templates.TemplateModule;
+import com.ning.billing.util.glue.AuditModule;
 import com.ning.billing.util.glue.BusModule;
 import com.ning.billing.util.glue.CallContextModule;
 import com.ning.billing.util.glue.ClockModule;
@@ -91,6 +92,7 @@ public class KillbillServerModule extends AbstractModule {
         install(new GlobalLockerModule());
         install(new CustomFieldModule());
         install(new TagStoreModule());
+        install(new AuditModule());
         install(new CatalogModule());
         install(new BusModule());
         install(new NotificationQueueModule());
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
index 749e108..94ae280 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestBundle.java
@@ -24,11 +24,15 @@ import java.util.Map;
 
 import javax.ws.rs.core.Response.Status;
 
+import org.joda.time.DateTime;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.ProductCategory;
 import com.ning.billing.jaxrs.json.AccountJson;
 import com.ning.billing.jaxrs.json.BundleJsonNoSubscriptions;
+import com.ning.billing.jaxrs.json.SubscriptionJsonNoEvents;
 import com.ning.billing.jaxrs.resources.JaxrsResource;
 import com.ning.http.client.Response;
 
@@ -107,5 +111,34 @@ public class TestBundle extends TestJaxrsBase {
         Assert.assertEquals(response.getStatusCode(), Status.NOT_FOUND.getStatusCode());
     }
 
+    @Test(groups = "slow", enabled = true)
+    public void testBundleTransfer() throws Exception {
+
+        final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final AccountJson accountJson = createAccountWithDefaultPaymentMethod("src", "src", "src@yahoo.com");
+        final BundleJsonNoSubscriptions bundleJson = createBundle(accountJson.getAccountId(), "93199");
+
+        final String productName = "Shotgun";
+        final BillingPeriod term = BillingPeriod.MONTHLY;
+
+        final SubscriptionJsonNoEvents subscriptionJson = createSubscription(bundleJson.getBundleId(), productName, ProductCategory.BASE.toString(), term.toString(), true);
+        Assert.assertNotNull(subscriptionJson.getChargedThroughDate());
+        Assert.assertEquals(subscriptionJson.getChargedThroughDate().toString(), "2012-04-25T00:00:00.000Z");
 
+        final AccountJson newAccount = createAccountWithDefaultPaymentMethod("dst", "dst", "dst@yahoo.com");
+
+        final BundleJsonNoSubscriptions newBundleInput = new BundleJsonNoSubscriptions(null, newAccount.getAccountId(), null, null, null);
+        final String newBundleInputJson = mapper.writeValueAsString(newBundleInput);
+        final String uri = JaxrsResource.BUNDLES_PATH + "/" + bundleJson.getBundleId();
+        Response response = doPut(uri, newBundleInputJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
+
+        final String locationCC = response.getHeader("Location");
+        Assert.assertNotNull(locationCC);
+
+        response = doGetWithUrl(locationCC, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
+        Assert.assertEquals(response.getStatusCode(), Status.OK.getStatusCode());
+    }
 }
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java b/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
index c20e0fe..c472456 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestChargeback.java
@@ -52,7 +52,7 @@ public class TestChargeback extends TestJaxrsBase {
     @Test(groups = "slow")
     public void testAddChargeback() throws Exception {
         final PaymentJsonSimple payment = createAccountWithInvoiceAndPayment();
-        final ChargebackJson input = new ChargebackJson(null, null, BigDecimal.TEN, payment.getPaymentId(), null);
+        final ChargebackJson input = new ChargebackJson(null, null, BigDecimal.TEN, payment.getPaymentId(), null, null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Create the chargeback
@@ -81,7 +81,7 @@ public class TestChargeback extends TestJaxrsBase {
         final PaymentJsonSimple payment = createAccountWithInvoiceAndPayment();
 
         // We get a 249.95 payment so we do 4 chargeback and then the fifth should fail
-        final ChargebackJson input = new ChargebackJson(null, null, new BigDecimal("50.00"), payment.getPaymentId(), null);
+        final ChargebackJson input = new ChargebackJson(null, null, new BigDecimal("50.00"), payment.getPaymentId(), null, null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         //
@@ -135,7 +135,8 @@ public class TestChargeback extends TestJaxrsBase {
     @Test(groups = "slow")
     public void testInvoicePaymentDoesNotExist() throws Exception {
         final ChargebackJson input = new ChargebackJson(new DateTime(DateTimeZone.UTC), new DateTime(DateTimeZone.UTC),
-                                                        BigDecimal.TEN, UUID.randomUUID().toString(), UUID.randomUUID().toString());
+                                                        BigDecimal.TEN, UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+                                                        null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Try to create the chargeback
@@ -145,7 +146,7 @@ public class TestChargeback extends TestJaxrsBase {
 
     @Test(groups = "slow")
     public void testBadRequest() throws Exception {
-        final ChargebackJson input = new ChargebackJson(null, null, null, null, null);
+        final ChargebackJson input = new ChargebackJson(null, null, null, null, null, null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Try to create the chargeback
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
index dc67b41..d7b5dca 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestCredit.java
@@ -62,7 +62,8 @@ public class TestCredit extends TestJaxrsBase {
         final InvoiceJsonSimple invoice = createInvoice();
         final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.fromString(invoice.getInvoiceId()), UUID.randomUUID().toString(),
                                                 requestedDate, effectiveDate,
-                                                UUID.randomUUID().toString(), UUID.fromString(accountJson.getAccountId()));
+                                                UUID.randomUUID().toString(), UUID.fromString(accountJson.getAccountId()),
+                                                null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Create the credit
@@ -121,7 +122,7 @@ public class TestCredit extends TestJaxrsBase {
         final DateTime effectiveDate = clock.getUTCNow();
         final CreditJson input = new CreditJson(BigDecimal.TEN, UUID.randomUUID(), UUID.randomUUID().toString(),
                                                 requestedDate, effectiveDate,
-                                                UUID.randomUUID().toString(), UUID.randomUUID());
+                                                UUID.randomUUID().toString(), UUID.randomUUID(), null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Try to create the credit
@@ -131,7 +132,7 @@ public class TestCredit extends TestJaxrsBase {
 
     @Test(groups = "slow")
     public void testBadRequest() throws Exception {
-        final CreditJson input = new CreditJson(null, null, null, null, null, null, null);
+        final CreditJson input = new CreditJson(null, null, null, null, null, null, null, null);
         final String jsonInput = mapper.writeValueAsString(input);
 
         // Try to create the credit
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
index 4efe6cb..4dfc5ab 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestInvoice.java
@@ -209,7 +209,8 @@ public class TestInvoice extends TestJaxrsBase {
             }
 
             // CREATE INSTA PAYMENT
-            final PaymentJsonSimple payment = new PaymentJsonSimple(cur.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(), cur.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null);
+            final PaymentJsonSimple payment = new PaymentJsonSimple(cur.getAmount(), BigDecimal.ZERO, accountJson.getAccountId(),
+                                                                    cur.getInvoiceId(), null, null, null, null, 0, null, null, null, null, null, null, null);
             final String postJson = mapper.writeValueAsString(payment);
 
             uri = JaxrsResource.INVOICES_PATH + "/" + cur.getInvoiceId() + "/" + JaxrsResource.PAYMENTS;
@@ -267,7 +268,7 @@ public class TestInvoice extends TestJaxrsBase {
         final BigDecimal paidAmount = BigDecimal.TEN;
         final PaymentJsonSimple payment = new PaymentJsonSimple(paidAmount, BigDecimal.ZERO, accountJson.getAccountId(),
                                                                 invoiceId, null, null, null, null, 0,
-                                                                null, null, null, null, null, null);
+                                                                null, null, null, null, null, null, null);
         final String postJson = mapper.writeValueAsString(payment);
         final String paymentURI = JaxrsResource.INVOICES_PATH + "/" + invoiceId + "/" + JaxrsResource.PAYMENTS;
         final Response paymentResponse = doPost(paymentURI, postJson, ImmutableMap.<String, String>of("externalPayment", "true"), DEFAULT_HTTP_TIMEOUT_SEC);
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
index 9153cd5..aba8426 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestJaxrsBase.java
@@ -81,6 +81,7 @@ import com.ning.billing.util.clock.Clock;
 import com.ning.billing.util.clock.ClockMock;
 import com.ning.billing.util.email.EmailModule;
 import com.ning.billing.util.email.templates.TemplateModule;
+import com.ning.billing.util.glue.AuditModule;
 import com.ning.billing.util.glue.BusModule;
 import com.ning.billing.util.glue.CallContextModule;
 import com.ning.billing.util.glue.CustomFieldModule;
@@ -209,6 +210,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
             install(new GlobalLockerModule());
             install(new CustomFieldModule());
             install(new TagStoreModule());
+            install(new AuditModule());
             install(new CatalogModule());
             install(new BusModule());
             install(new NotificationQueueModule());
@@ -370,7 +372,7 @@ public class TestJaxrsBase extends ServerTestSuiteWithEmbeddedDB {
 
 
     protected BundleJsonNoSubscriptions createBundle(final String accountId, final String key) throws Exception {
-        final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null);
+        final BundleJsonNoSubscriptions input = new BundleJsonNoSubscriptions(null, accountId, key, null, null);
         String baseJson = mapper.writeValueAsString(input);
         Response response = doPost(JaxrsResource.BUNDLES_PATH, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
         Assert.assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());
diff --git a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
index 0afc6b8..2b16d3f 100644
--- a/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
+++ b/server/src/test/java/com/ning/billing/jaxrs/TestPayment.java
@@ -92,7 +92,7 @@ public class TestPayment extends TestJaxrsBase {
 
         // Issue the refund
 
-        final RefundJson refundJson = new RefundJson(null, paymentId, paymentAmount, false, null, null);
+        final RefundJson refundJson = new RefundJson(null, paymentId, paymentAmount, false, null, null, null);
         baseJson = mapper.writeValueAsString(refundJson);
         response = doPost(uri, baseJson, DEFAULT_EMPTY_QUERY, DEFAULT_HTTP_TIMEOUT_SEC);
         assertEquals(response.getStatusCode(), Status.CREATED.getStatusCode());

usage/pom.xml 110(+110 -0)

diff --git a/usage/pom.xml b/usage/pom.xml
new file mode 100644
index 0000000..f97c4fa
--- /dev/null
+++ b/usage/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ~ Copyright 2010-2011 Ning, Inc. ~ ~ Ning licenses this file to you
+    under the Apache License, version 2.0 ~ (the "License"); you may not use
+    this file except in compliance with the ~ License. You may obtain a copy
+    of the License at: ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless
+    required by applicable law or agreed to in writing, software ~ distributed
+    under the License is distributed on an "AS IS" BASIS, WITHOUT ~ WARRANTIES
+    OR CONDITIONS OF ANY KIND, either express or implied. See the ~ License for
+    the specific language governing permissions and limitations ~ under the License. -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.ning.billing</groupId>
+        <artifactId>killbill</artifactId>
+        <version>0.1.26-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <artifactId>killbill-usage</artifactId>
+    <name>killbill-usage</name>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-joda</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-smile</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.util</groupId>
+            <artifactId>low-gc-membuffers</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.inject</groupId>
+            <artifactId>guice</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.ning.billing</groupId>
+            <artifactId>killbill-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.ning.billing</groupId>
+            <artifactId>killbill-util</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.ning.billing</groupId>
+            <artifactId>killbill-util</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.skife.config</groupId>
+            <artifactId>config-magic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.antlr</groupId>
+            <artifactId>stringtemplate</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-mxj</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-mxj-db-files</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java b/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java
new file mode 100644
index 0000000..8f1e6a9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/api/user/DefaultUsageUserApi.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.api.user;
+
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.api.UsageUserApi;
+import com.ning.billing.usage.dao.RolledUpUsageDao;
+import com.ning.billing.usage.timeline.TimelineEventHandler;
+import com.ning.billing.util.clock.Clock;
+
+import com.google.common.collect.ImmutableMap;
+
+public class DefaultUsageUserApi implements UsageUserApi {
+
+    private static final String DEFAULT_EVENT_TYPE = "__DefaultUsageUserApi__";
+
+    private final RolledUpUsageDao rolledUpUsageDao;
+    private final TimelineEventHandler timelineEventHandler;
+    private final Clock clock;
+
+    @Inject
+    public DefaultUsageUserApi(final RolledUpUsageDao rolledUpUsageDao, final TimelineEventHandler timelineEventHandler, final Clock clock) {
+        this.rolledUpUsageDao = rolledUpUsageDao;
+        this.timelineEventHandler = timelineEventHandler;
+        this.clock = clock;
+    }
+
+    @Override
+    public void incrementUsage(final UUID bundleId, final String metricName) {
+        recordUsage(bundleId, metricName, clock.getUTCNow(), 1);
+    }
+
+    @Override
+    public void recordUsage(final UUID bundleId, final String metricName, final DateTime timestamp, final long value) {
+        final String sourceName = getSourceNameFromBundleId(bundleId);
+        timelineEventHandler.record(sourceName, DEFAULT_EVENT_TYPE, timestamp, ImmutableMap.<String, Object>of(metricName, value));
+    }
+
+    @Override
+    public void recordRolledUpUsage(final UUID bundleId, final String metricName, final DateTime startDate, final DateTime endDate, final long value) {
+        final String sourceName = getSourceNameFromBundleId(bundleId);
+        rolledUpUsageDao.record(sourceName, DEFAULT_EVENT_TYPE, metricName, startDate, endDate, value);
+    }
+
+    private String getSourceNameFromBundleId(final UUID bundleId) {
+        // TODO we should do better
+        return bundleId.toString();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java b/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java
new file mode 100644
index 0000000..58e3dce
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/DefaultRolledUpUsageDao.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.dao;
+
+import javax.inject.Inject;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Transaction;
+import org.skife.jdbi.v2.TransactionStatus;
+
+import com.ning.billing.usage.timeline.persistent.TimelineSqlDao;
+
+public class DefaultRolledUpUsageDao implements RolledUpUsageDao {
+
+    private final RolledUpUsageSqlDao rolledUpUsageSqlDao;
+
+    @Inject
+    public DefaultRolledUpUsageDao(final RolledUpUsageSqlDao rolledUpUsageSqlDao) {
+        this.rolledUpUsageSqlDao = rolledUpUsageSqlDao;
+    }
+
+    @Override
+    public void record(final String source, final String eventType, final String metricName, final DateTime startDate, final DateTime endDate, final long value) {
+        rolledUpUsageSqlDao.inTransaction(new Transaction<Void, RolledUpUsageSqlDao>() {
+            @Override
+            public Void inTransaction(final RolledUpUsageSqlDao transactional, final TransactionStatus status) throws Exception {
+                final TimelineSqlDao timelineSqlDao = transactional.become(TimelineSqlDao.class);
+
+                // Create the source if it doesn't exist
+                Integer sourceId = timelineSqlDao.getSourceId(source);
+                if (sourceId == null) {
+                    timelineSqlDao.addSource(source);
+                    sourceId = timelineSqlDao.getSourceId(source);
+                }
+
+                // Create the category if it doesn't exist
+                Integer categoryId = timelineSqlDao.getEventCategoryId(eventType);
+                if (categoryId == null) {
+                    timelineSqlDao.addEventCategory(eventType);
+                    categoryId = timelineSqlDao.getEventCategoryId(eventType);
+                }
+
+                // Create the metric if it doesn't exist
+                Integer metricId = timelineSqlDao.getMetricId(categoryId, metricName);
+                if (metricId == null) {
+                    timelineSqlDao.addMetric(categoryId, metricName);
+                    metricId = timelineSqlDao.getMetricId(categoryId, metricName);
+                }
+
+                transactional.record(sourceId, metricId, startDate.toDate(), endDate.toDate(), value);
+
+                return null;
+            }
+        });
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java
new file mode 100644
index 0000000..a75a882
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.dao;
+
+import org.joda.time.DateTime;
+
+/**
+ * Dao to record already rolled-up usage data (rolled-up by the user).
+ * For raw tracking of the data, @see TimelineEventHandler.
+ */
+public interface RolledUpUsageDao {
+
+    public void record(final String sourceName, final String eventType, final String metricName, final DateTime startDate, final DateTime endDate, final long value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java
new file mode 100644
index 0000000..b495535
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/dao/RolledUpUsageSqlDao.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.dao;
+
+import java.util.Date;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+
+@ExternalizedSqlViaStringTemplate3()
+public interface RolledUpUsageSqlDao extends Transactional<RolledUpUsageSqlDao>, Transmogrifier {
+
+    @SqlUpdate
+    public void record(@Bind("sourceId") final int sourceId, @Bind("metricId") final int metricId,
+                       @Bind("startTime") final Date startTime, @Bind("endTime") final Date endTime,
+                       @Bind("value") final long value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java b/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java
new file mode 100644
index 0000000..6e53a63
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/glue/CachingDefaultTimelineDaoProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.glue;
+
+import javax.inject.Provider;
+
+import org.skife.jdbi.v2.DBI;
+
+import com.ning.billing.usage.timeline.persistent.CachingTimelineDao;
+import com.ning.billing.usage.timeline.persistent.DefaultTimelineDao;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+
+import com.google.inject.Inject;
+
+public class CachingDefaultTimelineDaoProvider implements Provider<TimelineDao> {
+
+    private final DBI dbi;
+
+    @Inject
+    public CachingDefaultTimelineDaoProvider(final DBI dbi) {
+        this.dbi = dbi;
+    }
+
+    @Override
+    public TimelineDao get() {
+        final TimelineDao delegate = new DefaultTimelineDao(dbi);
+
+        return new CachingTimelineDao(delegate);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java b/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java
new file mode 100644
index 0000000..d6ec3e8
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/glue/UsageModule.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.glue;
+
+import java.io.IOException;
+
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.codec.DefaultSampleCoder;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.persistent.FileBackedBuffer;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.inject.AbstractModule;
+
+public class UsageModule extends AbstractModule {
+
+    private final ConfigSource configSource;
+
+    public UsageModule() {
+        this(new SimplePropertyConfigSource(System.getProperties()));
+    }
+
+    public UsageModule(final ConfigSource configSource) {
+        this.configSource = configSource;
+    }
+
+    protected UsageConfig installConfig() {
+        final UsageConfig config = new ConfigurationObjectFactory(configSource).build(UsageConfig.class);
+        bind(UsageConfig.class).toInstance(config);
+
+        return config;
+    }
+
+    protected void configureFileBackedBuffer(final UsageConfig config) {
+        // Persistent buffer for in-memory samples
+        try {
+            final boolean deleteFilesOnClose = config.getShutdownSaveMode().equals("save_all_timelines");
+            final FileBackedBuffer fileBackedBuffer = new FileBackedBuffer(config.getSpoolDir(), "TimelineEventHandler", deleteFilesOnClose, config.getSegmentsSize(), config.getMaxNbSegments());
+            bind(FileBackedBuffer.class).toInstance(fileBackedBuffer);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected void configureDao() {
+        bind(TimelineDao.class).toProvider(CachingDefaultTimelineDaoProvider.class).asEagerSingleton();
+    }
+
+    protected void configureTimelineObjects() {
+        bind(TimelineCoder.class).to(DefaultTimelineCoder.class).asEagerSingleton();
+        bind(SampleCoder.class).to(DefaultSampleCoder.class).asEagerSingleton();
+    }
+
+    @Override
+    protected void configure() {
+        final UsageConfig config = installConfig();
+
+        configureFileBackedBuffer(config);
+        configureDao();
+        configureTimelineObjects();
+
+        // TODO
+        //configureTimelineAggregator();
+        //configureBackgroundDBChunkWriter();
+        //configureReplayer();
+    }
+}
+
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java
new file mode 100644
index 0000000..ad0b3bc
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregator.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.aggregator;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.ResultIterator;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkMapper;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.persistent.DefaultTimelineDao;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.inject.Inject;
+
+/**
+ * This class runs a thread that periodically looks for unaggregated timelines.
+ * When it finds them, it combines them intelligently as if they were originally
+ * a single sequence of times.
+ */
+public class TimelineAggregator {
+
+    private static final Logger log = LoggerFactory.getLogger(TimelineAggregator.class);
+
+    private final IDBI dbi;
+    private final DefaultTimelineDao timelineDao;
+    private final TimelineCoder timelineCoder;
+    private final SampleCoder sampleCoder;
+    private final UsageConfig config;
+    private final TimelineAggregatorSqlDao aggregatorSqlDao;
+    private final TimelineChunkMapper timelineChunkMapper;
+    private final ScheduledExecutorService aggregatorThread = Executors.newSingleThreadScheduledExecutor();
+
+    private Map<String, AtomicLong> aggregatorCounters = new LinkedHashMap<String, AtomicLong>();
+
+    private final AtomicBoolean isAggregating = new AtomicBoolean(false);
+
+    private final AtomicLong aggregationRuns = new AtomicLong();
+    private final AtomicLong foundNothingRuns = new AtomicLong();
+    private final AtomicLong aggregatesCreated = makeCounter("aggsCreated");
+    private final AtomicLong timelineChunksConsidered = makeCounter("chunksConsidered");
+    private final AtomicLong timelineChunkBatchesProcessed = makeCounter("batchesProcessed");
+    private final AtomicLong timelineChunksCombined = makeCounter("chunksCombined");
+    private final AtomicLong timelineChunksQueuedForCreation = makeCounter("chunksQueued");
+    private final AtomicLong timelineChunksWritten = makeCounter("chunksWritten");
+    private final AtomicLong timelineChunksInvalidatedOrDeleted = makeCounter("chunksInvalidatedOrDeleted");
+    private final AtomicLong timelineChunksBytesCreated = makeCounter("bytesCreated");
+    private final AtomicLong msSpentAggregating = makeCounter("msSpentAggregating");
+    private final AtomicLong msSpentSleeping = makeCounter("msSpentSleeping");
+    private final AtomicLong msWritingDb = makeCounter("msWritingDb");
+
+    // These lists support batching of aggregated chunk writes and updates or deletes of the chunks aggregated
+    private final List<TimelineChunk> chunksToWrite = new ArrayList<TimelineChunk>();
+    private final List<Long> chunkIdsToInvalidateOrDelete = new ArrayList<Long>();
+
+    @Inject
+    public TimelineAggregator(final IDBI dbi, final DefaultTimelineDao timelineDao, final TimelineCoder timelineCoder, final SampleCoder sampleCoder, final UsageConfig config) {
+        this.dbi = dbi;
+        this.timelineDao = timelineDao;
+        this.timelineCoder = timelineCoder;
+        this.sampleCoder = sampleCoder;
+        this.config = config;
+        this.aggregatorSqlDao = dbi.onDemand(TimelineAggregatorSqlDao.class);
+        this.timelineChunkMapper = new TimelineChunkMapper();
+    }
+
+    private int aggregateTimelineCandidates(final List<TimelineChunk> timelineChunkCandidates, final int aggregationLevel, final int chunksToAggregate) {
+        final TimelineChunk firstCandidate = timelineChunkCandidates.get(0);
+        final int sourceId = firstCandidate.getSourceId();
+        final int metricId = firstCandidate.getMetricId();
+        log.debug("For sourceId {}, metricId {}, looking to aggregate {} candidates in {} chunks",
+                  new Object[]{sourceId, metricId, timelineChunkCandidates.size(), chunksToAggregate});
+        int aggregatesCreated = 0;
+        int chunkIndex = 0;
+        while (timelineChunkCandidates.size() >= chunkIndex + chunksToAggregate) {
+            final List<TimelineChunk> chunkCandidates = timelineChunkCandidates.subList(chunkIndex, chunkIndex + chunksToAggregate);
+            chunkIndex += chunksToAggregate;
+            timelineChunksCombined.addAndGet(chunksToAggregate);
+            try {
+                aggregateHostSampleChunks(chunkCandidates, aggregationLevel);
+            } catch (IOException e) {
+                log.error(String.format("IOException aggregating {} chunks, sourceId %s, metricId %s, looking to aggregate %s candidates in %s chunks",
+                                        new Object[]{firstCandidate.getSourceId(), firstCandidate.getMetricId(), timelineChunkCandidates.size(), chunksToAggregate}), e);
+            }
+            aggregatesCreated++;
+        }
+
+        return aggregatesCreated;
+    }
+
+    /**
+     * The sequence of events is:
+     * <ul>
+     * <li>Build the aggregated TimelineChunk object, and save it, setting not_valid to true, and
+     * aggregation_level to 1.  This means that it won't be noticed by any of the dashboard
+     * queries.  The save operation returns the new timeline_times_id</li>
+     * <li>Then, in a single transaction, update the aggregated TimelineChunk object to have not_valid = 0,
+     * and also delete the TimelineChunk objects that were the basis of the aggregation, and flush
+     * any TimelineChunks that happen to be in the cache.</li>
+     * <p/>
+     *
+     * @param timelineChunks the TimelineChunks to be aggregated
+     */
+    private void aggregateHostSampleChunks(final List<TimelineChunk> timelineChunks, final int aggregationLevel) throws IOException {
+        final TimelineChunk firstTimesChunk = timelineChunks.get(0);
+        final TimelineChunk lastTimesChunk = timelineChunks.get(timelineChunks.size() - 1);
+        final int chunkCount = timelineChunks.size();
+        final int sourceId = firstTimesChunk.getSourceId();
+        final DateTime startTime = firstTimesChunk.getStartTime();
+        final DateTime endTime = lastTimesChunk.getEndTime();
+        final List<byte[]> timeParts = new ArrayList<byte[]>(chunkCount);
+        try {
+            final List<byte[]> sampleParts = new ArrayList<byte[]>(chunkCount);
+            final List<Long> timelineChunkIds = new ArrayList<Long>(chunkCount);
+            int sampleCount = 0;
+            for (final TimelineChunk timelineChunk : timelineChunks) {
+                timeParts.add(timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes());
+                sampleParts.add(timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes());
+                sampleCount += timelineChunk.getSampleCount();
+                timelineChunkIds.add(timelineChunk.getChunkId());
+            }
+            final byte[] combinedTimeBytes = timelineCoder.combineTimelines(timeParts, sampleCount);
+            final byte[] combinedSampleBytes = sampleCoder.combineSampleBytes(sampleParts);
+            final int timeBytesLength = combinedTimeBytes.length;
+            final int totalSize = 4 + timeBytesLength + combinedSampleBytes.length;
+            log.debug("For sourceId {}, aggregationLevel {}, aggregating {} timelines ({} bytes, {} samples): {}",
+                      new Object[]{firstTimesChunk.getSourceId(), firstTimesChunk.getAggregationLevel(), timelineChunks.size(), totalSize, sampleCount});
+            timelineChunksBytesCreated.addAndGet(totalSize);
+            final int totalSampleCount = sampleCount;
+            final TimelineChunk chunk = new TimelineChunk(0, sourceId, firstTimesChunk.getMetricId(), startTime, endTime,
+                                                          combinedTimeBytes, combinedSampleBytes, totalSampleCount, aggregationLevel + 1, false, false);
+            chunksToWrite.add(chunk);
+            chunkIdsToInvalidateOrDelete.addAll(timelineChunkIds);
+            timelineChunksQueuedForCreation.incrementAndGet();
+
+            if (chunkIdsToInvalidateOrDelete.size() >= config.getMaxChunkIdsToInvalidateOrDelete()) {
+                performWrites();
+            }
+        } catch (Exception e) {
+            log.error(String.format("Exception aggregating level %d, sourceId %d, metricId %d, startTime %s, endTime %s",
+                                    aggregationLevel, sourceId, firstTimesChunk.getMetricId(), startTime, endTime), e);
+        }
+    }
+
+    private void performWrites() {
+        // This is the atomic operation: bulk insert the new aggregated TimelineChunk objects, and delete
+        // or invalidate the ones that were aggregated.  This should be very fast.
+        final long startWriteTime = System.currentTimeMillis();
+        aggregatorSqlDao.begin();
+        timelineDao.bulkInsertTimelineChunks(chunksToWrite);
+        if (config.getDeleteAggregatedChunks()) {
+            aggregatorSqlDao.deleteTimelineChunks(chunkIdsToInvalidateOrDelete);
+        } else {
+            aggregatorSqlDao.makeTimelineChunksInvalid(chunkIdsToInvalidateOrDelete);
+        }
+        aggregatorSqlDao.commit();
+        msWritingDb.addAndGet(System.currentTimeMillis() - startWriteTime);
+
+        timelineChunksWritten.addAndGet(chunksToWrite.size());
+        timelineChunksInvalidatedOrDeleted.addAndGet(chunkIdsToInvalidateOrDelete.size());
+        chunksToWrite.clear();
+        chunkIdsToInvalidateOrDelete.clear();
+        final long sleepMs = config.getAggregationSleepBetweenBatches().getMillis();
+        if (sleepMs > 0) {
+            final long timeBeforeSleep = System.currentTimeMillis();
+            try {
+                Thread.sleep(sleepMs);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            msSpentSleeping.addAndGet(System.currentTimeMillis() - timeBeforeSleep);
+        }
+        timelineChunkBatchesProcessed.incrementAndGet();
+    }
+
+    /**
+     * This method aggregates candidate timelines
+     */
+    public void getAndProcessTimelineAggregationCandidates() {
+        if (!isAggregating.compareAndSet(false, true)) {
+            log.info("Asked to aggregate, but we're already aggregating!");
+            return;
+        } else {
+            log.debug("Starting aggregating");
+        }
+
+        aggregationRuns.incrementAndGet();
+        final String[] chunkCountsToAggregate = config.getChunksToAggregate().split(",");
+        for (int aggregationLevel = 0; aggregationLevel < config.getMaxAggregationLevel(); aggregationLevel++) {
+            final long startingAggregatesCreated = aggregatesCreated.get();
+            final Map<String, Long> initialCounters = captureAggregatorCounters();
+            final int chunkCountIndex = aggregationLevel >= chunkCountsToAggregate.length ? chunkCountsToAggregate.length - 1 : aggregationLevel;
+            final int chunksToAggregate = Integer.parseInt(chunkCountsToAggregate[chunkCountIndex]);
+            streamingAggregateLevel(aggregationLevel, chunksToAggregate);
+            final Map<String, Long> counterDeltas = subtractFromAggregatorCounters(initialCounters);
+            final long netAggregatesCreated = aggregatesCreated.get() - startingAggregatesCreated;
+            if (netAggregatesCreated == 0) {
+                if (aggregationLevel == 0) {
+                    foundNothingRuns.incrementAndGet();
+                }
+                log.debug("Created no new aggregates, so skipping higher-level aggregations");
+                break;
+            } else {
+                final StringBuilder builder = new StringBuilder();
+                builder
+                        .append("For aggregation level ")
+                        .append(aggregationLevel)
+                        .append(", runs ")
+                        .append(aggregationRuns.get())
+                        .append(", foundNothingRuns ")
+                        .append(foundNothingRuns.get());
+                for (final Map.Entry<String, Long> entry : counterDeltas.entrySet()) {
+                    builder.append(", ").append(entry.getKey()).append(": ").append(entry.getValue());
+                }
+                log.info(builder.toString());
+            }
+        }
+
+        log.debug("Aggregation done");
+        isAggregating.set(false);
+    }
+
+    private void streamingAggregateLevel(final int aggregationLevel, final int chunksToAggregate) {
+        final List<TimelineChunk> sourceTimelineCandidates = new ArrayList<TimelineChunk>();
+        final TimelineChunkConsumer aggregationConsumer = new TimelineChunkConsumer() {
+
+            int lastSourceId = 0;
+            int lastMetricId = 0;
+
+            @Override
+            public void processTimelineChunk(final TimelineChunk candidate) {
+                timelineChunksConsidered.incrementAndGet();
+                final int sourceId = candidate.getSourceId();
+                final int metricId = candidate.getMetricId();
+                if (lastSourceId == 0) {
+                    lastSourceId = sourceId;
+                    lastMetricId = metricId;
+                }
+                if (lastSourceId != sourceId || lastMetricId != metricId) {
+                    aggregatesCreated.addAndGet(aggregateTimelineCandidates(sourceTimelineCandidates, aggregationLevel, chunksToAggregate));
+                    sourceTimelineCandidates.clear();
+                    lastSourceId = sourceId;
+                    lastMetricId = metricId;
+                }
+                sourceTimelineCandidates.add(candidate);
+            }
+        };
+        final long startTime = System.currentTimeMillis();
+        try {
+            dbi.withHandle(new HandleCallback<Void>() {
+
+                @Override
+                public Void withHandle(final Handle handle) throws Exception {
+                    final Query<Map<String, Object>> query = handle.createQuery("getStreamingAggregationCandidates")
+                                                                   .setFetchSize(Integer.MIN_VALUE)
+                                                                   .bind("aggregationLevel", aggregationLevel);
+                    query.setStatementLocator(new StringTemplate3StatementLocator(TimelineAggregatorSqlDao.class));
+                    ResultIterator<TimelineChunk> iterator = null;
+                    try {
+                        iterator = query
+                                .map(timelineChunkMapper)
+                                .iterator();
+                        while (iterator.hasNext()) {
+                            aggregationConsumer.processTimelineChunk(iterator.next());
+                        }
+                    } catch (Exception e) {
+                        log.error(String.format("Exception during aggregation of level %d", aggregationLevel), e);
+                    } finally {
+                        if (iterator != null) {
+                            iterator.close();
+                        }
+                    }
+                    return null;
+                }
+
+            });
+            if (sourceTimelineCandidates.size() >= chunksToAggregate) {
+                aggregatesCreated.addAndGet(aggregateTimelineCandidates(sourceTimelineCandidates, aggregationLevel, chunksToAggregate));
+            }
+            if (chunkIdsToInvalidateOrDelete.size() > 0) {
+                performWrites();
+            }
+        } finally {
+            msSpentAggregating.addAndGet(System.currentTimeMillis() - startTime);
+        }
+    }
+
+    private AtomicLong makeCounter(final String counterName) {
+        final AtomicLong counter = new AtomicLong();
+        aggregatorCounters.put(counterName, counter);
+        return counter;
+    }
+
+    private Map<String, Long> captureAggregatorCounters() {
+        final Map<String, Long> counterValues = new LinkedHashMap<String, Long>();
+        for (final Map.Entry<String, AtomicLong> entry : aggregatorCounters.entrySet()) {
+            counterValues.put(entry.getKey(), entry.getValue().get());
+        }
+        return counterValues;
+    }
+
+    private Map<String, Long> subtractFromAggregatorCounters(final Map<String, Long> initialCounters) {
+        final Map<String, Long> counterValues = new LinkedHashMap<String, Long>();
+        for (final Map.Entry<String, AtomicLong> entry : aggregatorCounters.entrySet()) {
+            final String key = entry.getKey();
+            counterValues.put(key, entry.getValue().get() - initialCounters.get(key));
+        }
+        return counterValues;
+    }
+
+    public void runAggregationThread() {
+        aggregatorThread.scheduleWithFixedDelay(new Runnable() {
+            @Override
+            public void run() {
+                getAndProcessTimelineAggregationCandidates();
+            }
+        },
+                                                config.getAggregationInterval().getMillis(),
+                                                config.getAggregationInterval().getMillis(),
+                                                TimeUnit.MILLISECONDS);
+    }
+
+    public void stopAggregationThread() {
+        aggregatorThread.shutdown();
+    }
+
+    public long getAggregationRuns() {
+        return aggregationRuns.get();
+    }
+
+    public long getFoundNothingRuns() {
+        return foundNothingRuns.get();
+    }
+
+    public long getTimelineChunksConsidered() {
+        return timelineChunksConsidered.get();
+    }
+
+    public long getTimelineChunkBatchesProcessed() {
+        return timelineChunkBatchesProcessed.get();
+    }
+
+    public long getTimelineChunksCombined() {
+        return timelineChunksCombined.get();
+    }
+
+    public long getTimelineChunksQueuedForCreation() {
+        return timelineChunksQueuedForCreation.get();
+    }
+
+    public long getTimelineChunksWritten() {
+        return timelineChunksWritten.get();
+    }
+
+    public long getTimelineChunksInvalidatedOrDeleted() {
+        return timelineChunksInvalidatedOrDeleted.get();
+    }
+
+    public long getTimelineChunksBytesCreated() {
+        return timelineChunksBytesCreated.get();
+    }
+
+    public long getMsSpentAggregating() {
+        return msSpentAggregating.get();
+    }
+
+    public long getMsSpentSleeping() {
+        return msSpentSleeping.get();
+    }
+
+    public long getMsWritingDb() {
+        return msWritingDb.get();
+    }
+
+    public void initiateAggregation() {
+        log.info("Starting user-initiated aggregation");
+        Executors.newSingleThreadExecutor().execute(new Runnable() {
+
+            @Override
+            public void run() {
+                getAndProcessTimelineAggregationCandidates();
+            }
+        });
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java
new file mode 100644
index 0000000..6ba063d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.aggregator;
+
+import java.util.List;
+
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+import org.skife.jdbi.v2.unstable.BindIn;
+
+@ExternalizedSqlViaStringTemplate3()
+public interface TimelineAggregatorSqlDao extends Transactional<TimelineAggregatorSqlDao> {
+
+    @SqlQuery
+    int getLastInsertedId();
+
+    @SqlUpdate
+    void makeTimelineChunkValid(@Bind("chunkId") final long chunkId);
+
+    @SqlUpdate
+    void makeTimelineChunksInvalid(@BindIn("chunkIds") final List<Long> chunkIds);
+
+    @SqlUpdate
+    void deleteTimelineChunks(@BindIn("chunkIds") final List<Long> chunkIds);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java b/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java
new file mode 100644
index 0000000..a7ab352
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/BackgroundDBChunkWriter.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * This class runs a thread that batch-writes TimelineChunks to the db.
+ * This class is thread-safe, and only holds up threads that want to queue
+ * TimelineChunks for the time it takes to copy the ArrayList of PendingChunkMaps.
+ * <p/>
+ * The background writing thread is scheduled every few seconds, as controlled by
+ * config.getBackgroundWriteCheckInterval().  It writes the current inventory of
+ * chunks if there are at least config.getBackgroundWriteBatchSize()
+ * TimelineChunks to be written, or if the time since the last write exceeds
+ * config.getBackgroundWriteMaxDelay().
+ */
+@Singleton
+public class BackgroundDBChunkWriter {
+
+    private static final Logger log = LoggerFactory.getLogger(BackgroundDBChunkWriter.class);
+
+    private final TimelineDao timelineDAO;
+    private final UsageConfig config;
+    private final boolean performForegroundWrites;
+
+    private final AtomicInteger pendingChunkCount = new AtomicInteger();
+    private final AtomicBoolean shuttingDown = new AtomicBoolean();
+    private List<PendingChunkMap> pendingChunks = new ArrayList<PendingChunkMap>();
+    private DateTime lastWriteTime = new DateTime();
+    private AtomicBoolean doingWritesNow = new AtomicBoolean();
+    private final ScheduledExecutorService backgroundWriteThread = Executors.newSingleThreadScheduledExecutor();
+
+    private final AtomicLong maybePerformBackgroundWritesCount = new AtomicLong();
+    private final AtomicLong backgroundWritesCount = new AtomicLong();
+    private final AtomicLong pendingChunkMapsAdded = new AtomicLong();
+    private final AtomicLong pendingChunksAdded = new AtomicLong();
+    private final AtomicLong pendingChunkMapsWritten = new AtomicLong();
+    private final AtomicLong pendingChunksWritten = new AtomicLong();
+    private final AtomicLong pendingChunkMapsMarkedConsumed = new AtomicLong();
+    private final AtomicLong foregroundChunkMapsWritten = new AtomicLong();
+    private final AtomicLong foregroundChunksWritten = new AtomicLong();
+
+    @Inject
+    public BackgroundDBChunkWriter(final TimelineDao timelineDAO, final UsageConfig config) {
+        this(timelineDAO, config, config.getPerformForegroundWrites());
+    }
+
+    public BackgroundDBChunkWriter(final TimelineDao timelineDAO, @Nullable final UsageConfig config, final boolean performForegroundWrites) {
+        this.timelineDAO = timelineDAO;
+        this.config = config;
+        this.performForegroundWrites = performForegroundWrites;
+    }
+
+    public synchronized void addPendingChunkMap(final PendingChunkMap chunkMap) {
+        if (shuttingDown.get()) {
+            log.error("In addPendingChunkMap(), but finishBackgroundWritingAndExit is true!");
+        } else {
+            if (performForegroundWrites) {
+                foregroundChunkMapsWritten.incrementAndGet();
+                final List<TimelineChunk> chunksToWrite = new ArrayList<TimelineChunk>(chunkMap.getChunkMap().values());
+                foregroundChunksWritten.addAndGet(chunksToWrite.size());
+                timelineDAO.bulkInsertTimelineChunks(chunksToWrite);
+                chunkMap.getAccumulator().markPendingChunkMapConsumed(chunkMap.getPendingChunkMapId());
+            } else {
+                pendingChunkMapsAdded.incrementAndGet();
+                final int chunkCount = chunkMap.getChunkCount();
+                pendingChunksAdded.addAndGet(chunkCount);
+                pendingChunks.add(chunkMap);
+                pendingChunkCount.addAndGet(chunkCount);
+            }
+        }
+    }
+
+    private void performBackgroundWrites() {
+        backgroundWritesCount.incrementAndGet();
+        List<PendingChunkMap> chunkMapsToWrite = null;
+        synchronized (this) {
+            chunkMapsToWrite = pendingChunks;
+            pendingChunks = new ArrayList<PendingChunkMap>();
+            pendingChunkCount.set(0);
+        }
+        final List<TimelineChunk> chunks = new ArrayList<TimelineChunk>();
+        for (final PendingChunkMap map : chunkMapsToWrite) {
+            pendingChunkMapsWritten.incrementAndGet();
+            pendingChunksWritten.addAndGet(map.getChunkMap().size());
+            chunks.addAll(map.getChunkMap().values());
+        }
+        timelineDAO.bulkInsertTimelineChunks(chunks);
+        for (final PendingChunkMap map : chunkMapsToWrite) {
+            pendingChunkMapsMarkedConsumed.incrementAndGet();
+            map.getAccumulator().markPendingChunkMapConsumed(map.getPendingChunkMapId());
+        }
+    }
+
+    private void maybePerformBackgroundWrites() {
+        // If already running background writes, just return
+        maybePerformBackgroundWritesCount.incrementAndGet();
+        if (!doingWritesNow.compareAndSet(false, true)) {
+            return;
+        } else {
+            try {
+                if (shuttingDown.get()) {
+                    performBackgroundWrites();
+                }
+                final int pendingCount = pendingChunkCount.get();
+                if (pendingCount > 0) {
+                    if (pendingCount >= config.getBackgroundWriteBatchSize() ||
+                            new DateTime().isBefore(lastWriteTime.plusMillis((int) config.getBackgroundWriteMaxDelay().getMillis()))) {
+                        performBackgroundWrites();
+                        lastWriteTime = new DateTime();
+                    }
+                }
+            } finally {
+                doingWritesNow.set(false);
+            }
+        }
+    }
+
+    public synchronized boolean getShutdownFinished() {
+        return !doingWritesNow.get() && pendingChunks.size() == 0;
+    }
+
+    public void initiateShutdown() {
+        shuttingDown.set(true);
+    }
+
+    public void runBackgroundWriteThread() {
+        if (!performForegroundWrites) {
+            backgroundWriteThread.scheduleWithFixedDelay(new Runnable() {
+                @Override
+                public void run() {
+                    maybePerformBackgroundWrites();
+                }
+            },
+                                                         config.getBackgroundWriteCheckInterval().getMillis(),
+                                                         config.getBackgroundWriteCheckInterval().getMillis(),
+                                                         TimeUnit.MILLISECONDS);
+        }
+    }
+
+    public void stopBackgroundWriteThread() {
+        if (!performForegroundWrites) {
+            backgroundWriteThread.shutdown();
+        }
+    }
+
+    public long getMaybePerformBackgroundWritesCount() {
+        return maybePerformBackgroundWritesCount.get();
+    }
+
+    public long getBackgroundWritesCount() {
+        return backgroundWritesCount.get();
+    }
+
+    public long getPendingChunkMapsAdded() {
+        return pendingChunkMapsAdded.get();
+    }
+
+    public long getPendingChunksAdded() {
+        return pendingChunksAdded.get();
+    }
+
+    public long getPendingChunkMapsWritten() {
+        return pendingChunkMapsWritten.get();
+    }
+
+    public long getPendingChunksWritten() {
+        return pendingChunksWritten.get();
+    }
+
+    public long getPendingChunkMapsMarkedConsumed() {
+        return pendingChunkMapsMarkedConsumed.get();
+    }
+
+    public long getForegroundChunkMapsWritten() {
+        return foregroundChunkMapsWritten.get();
+    }
+
+    public long getForegroundChunksWritten() {
+        return foregroundChunksWritten.get();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java
new file mode 100644
index 0000000..c1ba8b5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetrics.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CategoryAndMetrics implements Comparable<CategoryAndMetrics> {
+
+    @JsonProperty
+    private final String eventCategory;
+    @JsonProperty
+    private final Set<String> metrics = new HashSet<String>();
+
+    public CategoryAndMetrics(final String eventCategory) {
+        this.eventCategory = eventCategory;
+    }
+
+    @JsonCreator
+    public CategoryAndMetrics(@JsonProperty("eventCategory") final String eventCategory, @JsonProperty("metrics") final List<String> metrics) {
+        this.eventCategory = eventCategory;
+        this.metrics.addAll(metrics);
+    }
+
+    public void addMetric(final String metric) {
+        metrics.add(metric);
+    }
+
+    public String getEventCategory() {
+        return eventCategory;
+    }
+
+    public Set<String> getMetrics() {
+        return metrics;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("CategoryAndMetrics");
+        sb.append("{eventCategory='").append(eventCategory).append('\'');
+        sb.append(", metrics=").append(metrics);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final CategoryAndMetrics that = (CategoryAndMetrics) o;
+
+        if (!eventCategory.equals(that.eventCategory)) {
+            return false;
+        }
+        if (!metrics.equals(that.metrics)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = eventCategory.hashCode();
+        result = 31 * result + metrics.hashCode();
+        return result;
+    }
+
+    @Override
+    public int compareTo(final CategoryAndMetrics o) {
+        final int categoryComparison = eventCategory.compareTo(o.getEventCategory());
+        if (categoryComparison != 0) {
+            return categoryComparison;
+        } else {
+            if (metrics.size() > o.getMetrics().size()) {
+                return 1;
+            } else if (metrics.size() < o.getMetrics().size()) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java
new file mode 100644
index 0000000..3126db6
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryAndMetricsForSources.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class CategoryAndMetricsForSources implements Comparable<CategoryAndMetricsForSources> {
+
+    @JsonProperty
+    private final CategoryAndMetrics categoryAndMetrics;
+    @JsonProperty
+    private final Set<String> sources;
+
+    public CategoryAndMetricsForSources(final String eventCategory) {
+        this(new CategoryAndMetrics(eventCategory), new TreeSet<String>());
+    }
+
+    @JsonCreator
+    public CategoryAndMetricsForSources(@JsonProperty("categoryAndMetrics") final CategoryAndMetrics categoryAndMetrics, @JsonProperty("sources") final Set<String> sources) {
+        this.categoryAndMetrics = categoryAndMetrics;
+        this.sources = sources;
+    }
+
+    public void add(final String metric, final String source) {
+        categoryAndMetrics.addMetric(metric);
+        sources.add(source);
+    }
+
+    public CategoryAndMetrics getCategoryAndMetrics() {
+        return categoryAndMetrics;
+    }
+
+    public Set<String> getSources() {
+        return sources;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("CategoryAndMetricsForSources");
+        sb.append("{categoryAndMetrics=").append(categoryAndMetrics);
+        sb.append(", sources=").append(sources);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final CategoryAndMetricsForSources that = (CategoryAndMetricsForSources) o;
+
+        if (categoryAndMetrics != null ? !categoryAndMetrics.equals(that.categoryAndMetrics) : that.categoryAndMetrics != null) {
+            return false;
+        }
+        if (sources != null ? !sources.equals(that.sources) : that.sources != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = categoryAndMetrics != null ? categoryAndMetrics.hashCode() : 0;
+        result = 31 * result + (sources != null ? sources.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public int compareTo(final CategoryAndMetricsForSources o) {
+        return categoryAndMetrics.compareTo(o.getCategoryAndMetrics());
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java
new file mode 100644
index 0000000..30d58bb
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetric.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class CategoryIdAndMetric {
+
+    private final int eventCategoryId;
+    private final String metric;
+
+    public CategoryIdAndMetric(final int eventCategoryId, final String metric) {
+        this.eventCategoryId = eventCategoryId;
+        this.metric = metric;
+    }
+
+    public int getEventCategoryId() {
+        return eventCategoryId;
+    }
+
+    public String getMetric() {
+        return metric;
+    }
+
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null || !(other instanceof CategoryIdAndMetric)) {
+            return false;
+        } else {
+            final CategoryIdAndMetric typedOther = (CategoryIdAndMetric) other;
+            return eventCategoryId == typedOther.getEventCategoryId() && metric.equals(typedOther.getMetric());
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return eventCategoryId ^ metric.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("EventCategoryIdAndMetric(eventCategoryId %d, metric %s)", eventCategoryId, metric);
+    }
+
+    public static List<String> extractMetrics(final Collection<CategoryIdAndMetric> categoryIdsAndMetrics) {
+        final List<String> metrics = new ArrayList<String>();
+        for (final CategoryIdAndMetric categoryIdAndMetric : categoryIdsAndMetrics) {
+            metrics.add(categoryIdAndMetric.getMetric());
+        }
+        return metrics;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java
new file mode 100644
index 0000000..1c75f83
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricBinder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricBinder.CategoryIdAndMetricBinderFactory;
+
+@BindingAnnotation(CategoryIdAndMetricBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface CategoryIdAndMetricBinder {
+
+    public static class CategoryIdAndMetricBinderFactory implements BinderFactory {
+
+        public Binder build(final Annotation annotation) {
+            return new Binder<CategoryIdAndMetricBinder, CategoryIdAndMetric>() {
+                public void bind(final SQLStatement query, final CategoryIdAndMetricBinder binder, final CategoryIdAndMetric categoryAndKind) {
+                    query.bind("eventCategoryId", categoryAndKind.getEventCategoryId())
+                         .bind("metric", categoryAndKind.getMetric());
+                }
+            };
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java
new file mode 100644
index 0000000..5610f7d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/categories/CategoryIdAndMetricMapper.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class CategoryIdAndMetricMapper implements ResultSetMapper<CategoryIdAndMetric> {
+
+    @Override
+    public CategoryIdAndMetric map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+        final int eventCategoryId = rs.getInt("event_category_id");
+        final String metric = rs.getString("sample_kind");
+        return new CategoryIdAndMetric(eventCategoryId, metric);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java
new file mode 100644
index 0000000..6d062b9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimeBytesAndSampleBytes.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import java.util.Arrays;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonView;
+
+/**
+ * POJO containing a series of bytes and associated time points
+ */
+public class TimeBytesAndSampleBytes {
+
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final byte[] timeBytes;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final byte[] sampleBytes;
+
+    public TimeBytesAndSampleBytes(final byte[] timeBytes, final byte[] sampleBytes) {
+        this.timeBytes = timeBytes;
+        this.sampleBytes = sampleBytes;
+    }
+
+    public byte[] getTimeBytes() {
+        return timeBytes;
+    }
+
+    public byte[] getSampleBytes() {
+        return sampleBytes;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("TimeBytesAndSampleBytes");
+        sb.append("{timeBytes=").append(timeBytes == null ? "null" : "");
+        for (int i = 0; timeBytes != null && i < timeBytes.length; ++i) {
+            sb.append(i == 0 ? "" : ", ").append(timeBytes[i]);
+        }
+        sb.append(", sampleBytes=").append(sampleBytes == null ? "null" : "");
+        for (int i = 0; sampleBytes != null && i < sampleBytes.length; ++i) {
+            sb.append(i == 0 ? "" : ", ").append(sampleBytes[i]);
+        }
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final TimeBytesAndSampleBytes that = (TimeBytesAndSampleBytes) o;
+
+        if (!Arrays.equals(sampleBytes, that.sampleBytes)) {
+            return false;
+        }
+        if (!Arrays.equals(timeBytes, that.timeBytes)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = timeBytes != null ? Arrays.hashCode(timeBytes) : 0;
+        result = 31 * result + (sampleBytes != null ? Arrays.hashCode(sampleBytes) : 0);
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java
new file mode 100644
index 0000000..835022e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunk.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import org.joda.time.DateTime;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonView;
+
+/**
+ * Instances of this class represent timeline sequences read from the database
+ * for a single source and single metric.  The samples are held in a byte
+ * array.
+ */
+public class TimelineChunk {
+
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Base.class)
+    private final long chunkId;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Base.class)
+    private final int sourceId;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Base.class)
+    private final int metricId;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final DateTime startTime;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final DateTime endTime;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final TimeBytesAndSampleBytes timeBytesAndSampleBytes;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final int sampleCount;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final int aggregationLevel;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final boolean notValid;
+    @JsonProperty
+    @JsonView(TimelineChunksViews.Compact.class)
+    private final boolean dontAggregate;
+
+    public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+                         final byte[] times, final byte[] samples, final int sampleCount) {
+        this(chunkId, sourceId, metricId, startTime, endTime, times, samples, sampleCount, 0, false, false);
+    }
+
+    public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+                         final byte[] times, final byte[] samples, final int sampleCount, final int aggregationLevel, final boolean notValid, final boolean dontAggregate) {
+        this(chunkId, sourceId, metricId, startTime, endTime, new TimeBytesAndSampleBytes(times, samples), sampleCount,
+             aggregationLevel, notValid, dontAggregate);
+    }
+
+    public TimelineChunk(final long chunkId, final int sourceId, final int metricId, final DateTime startTime, final DateTime endTime,
+                         final TimeBytesAndSampleBytes timeBytesAndSampleBytes, final int sampleCount, final int aggregationLevel, final boolean notValid, final boolean dontAggregate) {
+        this.chunkId = chunkId;
+        this.sourceId = sourceId;
+        this.metricId = metricId;
+        this.startTime = startTime;
+        this.endTime = endTime;
+        this.timeBytesAndSampleBytes = timeBytesAndSampleBytes;
+        this.sampleCount = sampleCount;
+        this.aggregationLevel = aggregationLevel;
+        this.notValid = notValid;
+        this.dontAggregate = dontAggregate;
+    }
+
+    public TimelineChunk(final long chunkId, final TimelineChunk other) {
+        this(chunkId, other.getSourceId(), other.getMetricId(), other.getStartTime(), other.getEndTime(), other.getTimeBytesAndSampleBytes(),
+             other.getSampleCount(), other.getAggregationLevel(), other.getNotValid(), other.getDontAggregate());
+    }
+
+    public long getChunkId() {
+        return chunkId;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    public int getMetricId() {
+        return metricId;
+    }
+
+    public DateTime getStartTime() {
+        return startTime;
+    }
+
+    public DateTime getEndTime() {
+        return endTime;
+    }
+
+    public TimeBytesAndSampleBytes getTimeBytesAndSampleBytes() {
+        return timeBytesAndSampleBytes;
+    }
+
+    public int getSampleCount() {
+        return sampleCount;
+    }
+
+    public int getAggregationLevel() {
+        return aggregationLevel;
+    }
+
+    public boolean getNotValid() {
+        return notValid;
+    }
+
+    public boolean getDontAggregate() {
+        return dontAggregate;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("TimelineChunk");
+        sb.append("{chunkId=").append(chunkId);
+        sb.append(", sourceId=").append(sourceId);
+        sb.append(", metricId=").append(metricId);
+        sb.append(", startTime=").append(startTime);
+        sb.append(", endTime=").append(endTime);
+        sb.append(", timeBytesAndSampleBytes=").append(timeBytesAndSampleBytes);
+        sb.append(", sampleCount=").append(sampleCount);
+        sb.append(", aggregationLevel=").append(aggregationLevel);
+        sb.append(", notValid=").append(notValid);
+        sb.append(", dontAggregate=").append(dontAggregate);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final TimelineChunk that = (TimelineChunk) o;
+
+        if (aggregationLevel != that.aggregationLevel) {
+            return false;
+        }
+        if (chunkId != that.chunkId) {
+            return false;
+        }
+        if (dontAggregate != that.dontAggregate) {
+            return false;
+        }
+        if (metricId != that.metricId) {
+            return false;
+        }
+        if (notValid != that.notValid) {
+            return false;
+        }
+        if (sampleCount != that.sampleCount) {
+            return false;
+        }
+        if (sourceId != that.sourceId) {
+            return false;
+        }
+        if (endTime != null ? !endTime.equals(that.endTime) : that.endTime != null) {
+            return false;
+        }
+        if (startTime != null ? !startTime.equals(that.startTime) : that.startTime != null) {
+            return false;
+        }
+        if (timeBytesAndSampleBytes != null ? !timeBytesAndSampleBytes.equals(that.timeBytesAndSampleBytes) : that.timeBytesAndSampleBytes != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (chunkId ^ (chunkId >>> 32));
+        result = 31 * result + sourceId;
+        result = 31 * result + metricId;
+        result = 31 * result + (startTime != null ? startTime.hashCode() : 0);
+        result = 31 * result + (endTime != null ? endTime.hashCode() : 0);
+        result = 31 * result + (timeBytesAndSampleBytes != null ? timeBytesAndSampleBytes.hashCode() : 0);
+        result = 31 * result + sampleCount;
+        result = 31 * result + aggregationLevel;
+        result = 31 * result + (notValid ? 1 : 0);
+        result = 31 * result + (dontAggregate ? 1 : 0);
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java
new file mode 100644
index 0000000..60d7d25
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkBinder.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.sql.Types;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunkBinder.TimelineChunkBinderFactory;
+import com.ning.billing.usage.timeline.codec.TimesAndSamplesCoder;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+/**
+ * jdbi binder for TimelineChunk
+ */
+@BindingAnnotation(TimelineChunkBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface TimelineChunkBinder {
+
+    public static class TimelineChunkBinderFactory implements BinderFactory {
+
+        // Maximum size in bytes for a series of timesAndSamples to stay "in row" (stored as VARBINARY).
+        // Past this threshold, data is stored as a BLOB.
+        private static final int MAX_IN_ROW_BLOB_SIZE = 400;
+
+        public Binder build(final Annotation annotation) {
+            return new Binder<TimelineChunkBinder, TimelineChunk>() {
+                public void bind(final SQLStatement query, final TimelineChunkBinder binder, final TimelineChunk timelineChunk) {
+                    query.bind("sourceId", timelineChunk.getSourceId())
+                         .bind("metricId", timelineChunk.getMetricId())
+                         .bind("sampleCount", timelineChunk.getSampleCount())
+                         .bind("startTime", DateTimeUtils.unixSeconds(timelineChunk.getStartTime()))
+                         .bind("endTime", DateTimeUtils.unixSeconds(timelineChunk.getEndTime()))
+                         .bind("aggregationLevel", timelineChunk.getAggregationLevel())
+                         .bind("notValid", timelineChunk.getNotValid() ? 1 : 0)
+                         .bind("dontAggregate", timelineChunk.getDontAggregate() ? 1 : 0);
+
+                    final byte[] times = timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes();
+                    final byte[] samples = timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes();
+                    final byte[] timesAndSamples = TimesAndSamplesCoder.combineTimesAndSamples(times, samples);
+                    if (timelineChunk.getChunkId() == 0) {
+                        query.bindNull("chunkId", Types.BIGINT);
+                    } else {
+                        query.bind("chunkId", timelineChunk.getChunkId());
+                    }
+
+                    if (timesAndSamples.length > MAX_IN_ROW_BLOB_SIZE) {
+                        query.bindNull("inRowSamples", Types.VARBINARY)
+                             .bind("blobSamples", timesAndSamples);
+                    } else {
+                        query.bind("inRowSamples", timesAndSamples)
+                             .bindNull("blobSamples", Types.BLOB);
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java
new file mode 100644
index 0000000..346b8be
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunkMapper.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import java.sql.Blob;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.usage.timeline.codec.TimesAndSamplesCoder;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+/**
+ * jdbi mapper for TimelineChunk
+ */
+public class TimelineChunkMapper implements ResultSetMapper<TimelineChunk> {
+
+    @Override
+    public TimelineChunk map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+        final int chunkId = rs.getInt("chunk_id");
+        final int sourceId = rs.getInt("source_id");
+        final int metricId = rs.getInt("sample_kind_id");
+        final int sampleCount = rs.getInt("sample_count");
+        final DateTime startTime = DateTimeUtils.dateTimeFromUnixSeconds(rs.getInt("start_time"));
+        final DateTime endTime = DateTimeUtils.dateTimeFromUnixSeconds(rs.getInt("end_time"));
+        final int aggregationLevel = rs.getInt("aggregation_level");
+        final boolean notValid = rs.getInt("not_valid") == 0 ? false : true;
+        final boolean dontAggregate = rs.getInt("dont_aggregate") == 0 ? false : true;
+
+        byte[] samplesAndTimes = rs.getBytes("in_row_samples");
+        if (rs.wasNull()) {
+            final Blob blobSamples = rs.getBlob("blob_samples");
+            if (rs.wasNull()) {
+                samplesAndTimes = new byte[4];
+            } else {
+                samplesAndTimes = blobSamples.getBytes(1, (int) blobSamples.length());
+            }
+        }
+
+        final TimeBytesAndSampleBytes bytesPair = TimesAndSamplesCoder.getTimesBytesAndSampleBytes(samplesAndTimes);
+        return new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, bytesPair, sampleCount,
+                                 aggregationLevel, notValid, dontAggregate);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java
new file mode 100644
index 0000000..782451e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/chunks/TimelineChunksViews.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+public class TimelineChunksViews {
+
+    public static class Base {
+
+    }
+
+    public static class Compact extends Base {
+
+    }
+
+    public static class Loose extends Base {
+
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java
new file mode 100644
index 0000000..43a0763
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/DefaultSampleCoder.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.HalfFloat;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCursor;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+/**
+ * Instances of this class encode sample streams.  In addition, this class
+ * contains a collection of static methods providing lower-level encoding plumbing
+ */
+@SuppressWarnings("unchecked")
+public class DefaultSampleCoder implements SampleCoder {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultSampleCoder.class);
+    private static final BigInteger BIGINTEGER_ZERO_VALUE = new BigInteger("0");
+    private static final ScalarSample<Void> DOUBLE_ZERO_SAMPLE = new ScalarSample<Void>(SampleOpcode.DOUBLE_ZERO, null);
+    private static final ScalarSample<Void> INT_ZERO_SAMPLE = new ScalarSample<Void>(SampleOpcode.INT_ZERO, null);
+
+    // TODO: Figure out if 1/200 is an acceptable level of inaccuracy
+    // For the HalfFloat, which has a 10-bit mantissa, this means that it could differ
+    // in the last 3 bits of the mantissa and still be treated as matching.
+    public static final double MAX_FRACTION_ERROR = 1.0 / 200.0;
+    public static final double HALF_MAX_FRACTION_ERROR = MAX_FRACTION_ERROR / 2.0;
+
+    private static final double MIN_BYTE_DOUBLE_VALUE = ((double) Byte.MIN_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+    private static final double MAX_BYTE_DOUBLE_VALUE = ((double) Byte.MAX_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+
+    private static final double MIN_SHORT_DOUBLE_VALUE = ((double) Short.MIN_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+    private static final double MAX_SHORT_DOUBLE_VALUE = ((double) Short.MAX_VALUE) * (1.0 + HALF_MAX_FRACTION_ERROR);
+
+    @SuppressWarnings("unused")
+    private static final double INVERSE_MAX_FRACTION_ERROR = 1.0 / MAX_FRACTION_ERROR;
+
+    @Override
+    public byte[] compressSamples(final List<ScalarSample> samples) {
+        final SampleAccumulator accumulator = new SampleAccumulator(this);
+        accumulator.addSampleList(samples);
+        return accumulator.getEncodedSamples().getEncodedBytes();
+    }
+
+    @Override
+    public List<ScalarSample> decompressSamples(final byte[] sampleBytes) throws IOException {
+        final List<ScalarSample> returnedSamples = new ArrayList<ScalarSample>();
+        final ByteArrayInputStream byteStream = new ByteArrayInputStream(sampleBytes);
+        final DataInputStream inputStream = new DataInputStream(byteStream);
+        while (true) {
+            final int opcodeByte;
+            opcodeByte = inputStream.read();
+            if (opcodeByte == -1) {
+                break; // At "eof"
+            }
+            final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+            switch (opcode) {
+                case REPEAT_BYTE:
+                case REPEAT_SHORT:
+                    final int repeatCount = opcode == SampleOpcode.REPEAT_BYTE ? inputStream.readUnsignedByte() : inputStream.readUnsignedShort();
+                    final SampleOpcode repeatedOpcode = SampleOpcode.getOpcodeFromIndex(inputStream.read());
+                    final Object value = decodeScalarValue(inputStream, repeatedOpcode);
+                    for (int i = 0; i < repeatCount; i++) {
+                        returnedSamples.add(new ScalarSample(repeatedOpcode, value));
+                    }
+                    break;
+                default:
+                    returnedSamples.add(new ScalarSample(opcode, decodeScalarValue(inputStream, opcode)));
+                    break;
+            }
+        }
+        return returnedSamples;
+    }
+
+
+    /**
+     * This method writes the binary encoding of the sample to the outputStream.  This encoding
+     * is the form saved in the db and scanned when read from the db.
+     *
+     * @param outputStream the stream to which bytes should be written
+     * @param sample       the sample to be written
+     */
+    @Override
+    public void encodeSample(final DataOutputStream outputStream, final SampleBase sample) {
+        final SampleOpcode opcode = sample.getOpcode();
+        try {
+            // First put out the opcode value
+            switch (opcode) {
+                case REPEAT_BYTE:
+                case REPEAT_SHORT:
+                    final RepeatSample r = (RepeatSample) sample;
+                    final ScalarSample repeatee = r.getSampleRepeated();
+                    outputStream.write(opcode.getOpcodeIndex());
+                    if (opcode == SampleOpcode.REPEAT_BYTE) {
+                        outputStream.write(r.getRepeatCount());
+                    } else {
+                        outputStream.writeShort(r.getRepeatCount());
+                    }
+                    encodeScalarValue(outputStream, repeatee.getOpcode(), repeatee.getSampleValue());
+                case NULL:
+                    break;
+                default:
+                    if (sample instanceof ScalarSample) {
+                        encodeScalarValue(outputStream, opcode, ((ScalarSample) sample).getSampleValue());
+                    } else {
+                        log.error("In encodeSample, opcode {} is not ScalarSample; instead {}", opcode.name(), sample.getClass().getName());
+                    }
+            }
+        } catch (IOException e) {
+            log.error(String.format("In encodeSample, IOException encoding opcode %s and value %s", opcode.name(), String.valueOf(sample)), e);
+        }
+    }
+
+    /**
+     * Output the scalar value into the output stream
+     *
+     * @param outputStream the stream to which bytes should be written
+     * @param value        the sample value, interpreted according to the opcode
+     */
+    @Override
+    public void encodeScalarValue(final DataOutputStream outputStream, final SampleOpcode opcode, final Object value) {
+        try {
+            outputStream.write(opcode.getOpcodeIndex());
+            switch (opcode) {
+                case NULL:
+                case DOUBLE_ZERO:
+                case INT_ZERO:
+                    break;
+                case BYTE:
+                case BYTE_FOR_DOUBLE:
+                    outputStream.writeByte((Byte) value);
+                    break;
+                case SHORT:
+                case SHORT_FOR_DOUBLE:
+                case HALF_FLOAT_FOR_DOUBLE:
+                    outputStream.writeShort((Short) value);
+                    break;
+                case INT:
+                    outputStream.writeInt((Integer) value);
+                    break;
+                case LONG:
+                    outputStream.writeLong((Long) value);
+                    break;
+                case FLOAT:
+                case FLOAT_FOR_DOUBLE:
+                    outputStream.writeFloat((Float) value);
+                    break;
+                case DOUBLE:
+                    outputStream.writeDouble((Double) value);
+                    break;
+                case STRING:
+                    final String s = (String) value;
+                    final byte[] bytes = s.getBytes("UTF-8");
+                    outputStream.writeShort(s.length());
+                    outputStream.write(bytes, 0, bytes.length);
+                    break;
+                case BIGINT:
+                    final String bs = value.toString();
+                    // Only support bigints whose length can be encoded as a short
+                    if (bs.length() > Short.MAX_VALUE) {
+                        throw new IllegalStateException(String.format("In DefaultSampleCoder.encodeScalarValue(), the string length of the BigInteger is %d; too large to be represented in a Short", bs.length()));
+                    }
+                    final byte[] bbytes = bs.getBytes("UTF-8");
+                    outputStream.writeShort(bs.length());
+                    outputStream.write(bbytes, 0, bbytes.length);
+                    break;
+                default:
+                    final String err = String.format("In encodeScalarSample, opcode %s is unrecognized", opcode.name());
+                    log.error(err);
+                    throw new IllegalArgumentException(err);
+            }
+        } catch (IOException e) {
+            log.error(String.format("In encodeScalarValue, IOException encoding opcode %s and value %s", opcode.name(), String.valueOf(value)), e);
+        }
+    }
+
+    /**
+     * This routine returns a ScalarSample that may have a smaller representation than the
+     * ScalarSample argument.  In particular, if tries hard to choose the most compact
+     * representation of double-precision values.
+     *
+     * @param sample A ScalarSample to be compressed
+     * @return Either the same ScalarSample is that input, for for some cases of opcode DOUBLE,
+     *         a more compact ScalarSample which when processed returns a double value.
+     */
+    @Override
+    public ScalarSample compressSample(final ScalarSample sample) {
+        switch (sample.getOpcode()) {
+            case INT:
+                final int intValue = (Integer) sample.getSampleValue();
+                if (intValue == 0) {
+                    return INT_ZERO_SAMPLE;
+                } else if (intValue >= Byte.MIN_VALUE && intValue <= Byte.MAX_VALUE) {
+                    return new ScalarSample(SampleOpcode.BYTE, (byte) intValue);
+                } else if (intValue >= Short.MIN_VALUE && intValue <= Short.MAX_VALUE) {
+                    return new ScalarSample(SampleOpcode.SHORT, (short) intValue);
+                } else {
+                    return sample;
+                }
+            case LONG:
+                final long longValue = (Long) sample.getSampleValue();
+                if (longValue == 0) {
+                    return INT_ZERO_SAMPLE;
+                } else if (longValue >= Byte.MIN_VALUE && longValue <= Byte.MAX_VALUE) {
+                    return new ScalarSample(SampleOpcode.BYTE, (byte) longValue);
+                } else if (longValue >= Short.MIN_VALUE && longValue <= Short.MAX_VALUE) {
+                    return new ScalarSample(SampleOpcode.SHORT, (short) longValue);
+                } else if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) {
+                    return new ScalarSample(SampleOpcode.INT, (int) longValue);
+                } else {
+                    return sample;
+                }
+            case BIGINT:
+                final BigInteger bigValue = (BigInteger) sample.getSampleValue();
+                if (bigValue.compareTo(BIGINTEGER_ZERO_VALUE) == 0) {
+                    return INT_ZERO_SAMPLE;
+                }
+                final int digits = 1 + bigValue.bitCount();
+                if (digits <= 8) {
+                    return new ScalarSample(SampleOpcode.BYTE, (byte) bigValue.intValue());
+                } else if (digits <= 16) {
+                    return new ScalarSample(SampleOpcode.SHORT, (short) bigValue.intValue());
+                } else if (digits <= 32) {
+                    return new ScalarSample(SampleOpcode.INT, bigValue.intValue());
+                } else if (digits <= 64) {
+                    return new ScalarSample(SampleOpcode.LONG, bigValue.longValue());
+                } else {
+                    return sample;
+                }
+            case FLOAT:
+                return encodeFloatOrDoubleSample(sample, (double) ((Float) sample.getSampleValue()));
+            case DOUBLE:
+                return encodeFloatOrDoubleSample(sample, (Double) sample.getSampleValue());
+            default:
+                return sample;
+        }
+    }
+
+    private ScalarSample encodeFloatOrDoubleSample(final ScalarSample sample, final double value) {
+        // We prefer representations in the following order: byte, HalfFloat, short, float and int
+        // The criterion for using each representation is the fractional error
+        if (value == 0.0) {
+            return DOUBLE_ZERO_SAMPLE;
+        }
+        final boolean integral = value >= MIN_SHORT_DOUBLE_VALUE && value <= MAX_SHORT_DOUBLE_VALUE && (Math.abs((value - (double) ((int) value)) / value) <= MAX_FRACTION_ERROR);
+        if (integral && value >= MIN_BYTE_DOUBLE_VALUE && value <= MAX_BYTE_DOUBLE_VALUE) {
+            return new ScalarSample<Byte>(SampleOpcode.BYTE_FOR_DOUBLE, (byte) value);
+        } else if (integral && value >= MIN_SHORT_DOUBLE_VALUE && value <= MAX_SHORT_DOUBLE_VALUE) {
+            return new ScalarSample<Short>(SampleOpcode.SHORT_FOR_DOUBLE, (short) value);
+        } else {
+            final int halfFloatValue = HalfFloat.fromFloat((float) value);
+            if ((Math.abs(value - HalfFloat.toFloat(halfFloatValue)) / value) <= MAX_FRACTION_ERROR) {
+                return new ScalarSample<Short>(SampleOpcode.HALF_FLOAT_FOR_DOUBLE, (short) halfFloatValue);
+            } else if (value >= Float.MIN_VALUE && value <= Float.MAX_VALUE) {
+                return new ScalarSample<Float>(SampleOpcode.FLOAT_FOR_DOUBLE, (float) value);
+            } else {
+                return sample;
+            }
+        }
+    }
+
+    @Override
+    public Object decodeScalarValue(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException {
+        switch (opcode) {
+            case NULL:
+                return null;
+            case DOUBLE_ZERO:
+                return 0.0;
+            case INT_ZERO:
+                return 0;
+            case BYTE:
+                return inputStream.readByte();
+            case SHORT:
+                return inputStream.readShort();
+            case INT:
+                return inputStream.readInt();
+            case LONG:
+                return inputStream.readLong();
+            case FLOAT:
+                return inputStream.readFloat();
+            case DOUBLE:
+                return inputStream.readDouble();
+            case STRING:
+                final short s = inputStream.readShort();
+                final byte[] bytes = new byte[s];
+                final int byteCount = inputStream.read(bytes, 0, s);
+                if (byteCount != s) {
+                    log.error("Reading string came up short");
+                }
+                return new String(bytes, "UTF-8");
+            case BIGINT:
+                final short bs = inputStream.readShort();
+                final byte[] bbytes = new byte[bs];
+                final int bbyteCount = inputStream.read(bbytes, 0, bs);
+                if (bbyteCount != bs) {
+                    log.error("Reading bigint came up short");
+                }
+                return new BigInteger(new String(bbytes, "UTF-8"), 10);
+            case BYTE_FOR_DOUBLE:
+                return (double) inputStream.readByte();
+            case SHORT_FOR_DOUBLE:
+                return (double) inputStream.readShort();
+            case FLOAT_FOR_DOUBLE:
+                final float floatForDouble = inputStream.readFloat();
+                return (double) floatForDouble;
+            case HALF_FLOAT_FOR_DOUBLE:
+                final float f = HalfFloat.toFloat(inputStream.readShort());
+                return (double) f;
+            default:
+                final String err = String.format("In decodeScalarSample, opcode %s unrecognized", opcode.name());
+                log.error(err);
+                throw new IllegalArgumentException(err);
+        }
+    }
+
+    /*
+     * This differs from decodeScalarValue because this delivers exactly the
+     * type in the byte stream.  Specifically, it does not convert the arg
+     * of *_FOR_DOUBLE int a Double()
+     */
+    private Object decodeOpcodeArg(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException {
+        switch (opcode) {
+            case NULL:
+                return null;
+            case DOUBLE_ZERO:
+                return 0.0;
+            case INT_ZERO:
+                return 0;
+            case BYTE:
+                return inputStream.readByte();
+            case SHORT:
+                return inputStream.readShort();
+            case INT:
+                return inputStream.readInt();
+            case LONG:
+                return inputStream.readLong();
+            case FLOAT:
+                return inputStream.readFloat();
+            case DOUBLE:
+                return inputStream.readDouble();
+            case STRING:
+                final short s = inputStream.readShort();
+                final byte[] bytes = new byte[s];
+                final int byteCount = inputStream.read(bytes, 0, s);
+                if (byteCount != s) {
+                    log.error("Reading string came up short");
+                }
+                return new String(bytes, "UTF-8");
+            case BIGINT:
+                final short bs = inputStream.readShort();
+                final byte[] bbytes = new byte[bs];
+                final int bbyteCount = inputStream.read(bbytes, 0, bs);
+                if (bbyteCount != bs) {
+                    log.error("Reading bigint came up short");
+                }
+                return new BigInteger(new String(bbytes, "UTF-8"), 10);
+            case BYTE_FOR_DOUBLE:
+                return inputStream.readByte();
+            case SHORT_FOR_DOUBLE:
+                return inputStream.readShort();
+            case FLOAT_FOR_DOUBLE:
+                return inputStream.readFloat();
+            case HALF_FLOAT_FOR_DOUBLE:
+                return inputStream.readShort();
+            default:
+                final String err = String.format("In decodeOpcodeArg(), opcode %s unrecognized", opcode.name());
+                log.error(err);
+                throw new IllegalArgumentException(err);
+        }
+    }
+
+    @Override
+    public double getMaxFractionError() {
+        return MAX_FRACTION_ERROR;
+    }
+
+    @Override
+    public byte[] combineSampleBytes(final List<byte[]> sampleBytesList) {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final DataOutputStream dataStream = new DataOutputStream(outputStream);
+        try {
+            SampleBase lastSample = null;
+            for (final byte[] samples : sampleBytesList) {
+                final ByteArrayInputStream byteStream = new ByteArrayInputStream(samples);
+                final DataInputStream byteDataStream = new DataInputStream(byteStream);
+                while (true) {
+                    final int opcodeByte = byteDataStream.read();
+                    if (opcodeByte == -1) {
+                        break;
+                    }
+                    final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+                    switch (opcode) {
+                        case REPEAT_BYTE:
+                        case REPEAT_SHORT:
+                            final int newRepeatCount = opcode == SampleOpcode.REPEAT_BYTE ? byteDataStream.read() : byteDataStream.readUnsignedShort();
+                            final SampleOpcode newRepeatedOpcode = SampleOpcode.getOpcodeFromIndex(byteDataStream.read());
+                            final Object newValue = decodeOpcodeArg(byteDataStream, newRepeatedOpcode);
+                            final ScalarSample newRepeatedSample = new ScalarSample(newRepeatedOpcode, newValue);
+                            if (lastSample == null) {
+                                lastSample = new RepeatSample(newRepeatCount, new ScalarSample(newRepeatedOpcode, newValue));
+                            } else if (lastSample instanceof RepeatSample) {
+                                final RepeatSample repeatSample = (RepeatSample) lastSample;
+                                final ScalarSample repeatedScalarSample = repeatSample.getSampleRepeated();
+                                if (repeatedScalarSample.getOpcode() == newRepeatedOpcode &&
+                                        (newRepeatedOpcode.getNoArgs() ||
+                                                (ScalarSample.sameSampleValues(repeatedScalarSample.getSampleValue(), newValue) &&
+                                                        repeatSample.getRepeatCount() + newRepeatCount < RepeatSample.MAX_SHORT_REPEAT_COUNT))) {
+                                    // We can just increment the count in the repeat instance
+                                    repeatSample.incrementRepeatCount(newRepeatCount);
+                                } else {
+                                    encodeSample(dataStream, lastSample);
+                                    lastSample = new RepeatSample(newRepeatCount, newRepeatedSample);
+                                }
+                            } else if (lastSample.equals(newRepeatedSample)) {
+                                lastSample = new RepeatSample(newRepeatCount + 1, newRepeatedSample);
+                            } else {
+                                encodeSample(dataStream, lastSample);
+                                lastSample = new RepeatSample(newRepeatCount, newRepeatedSample);
+                            }
+                            break;
+                        default:
+                            final ScalarSample newSample = new ScalarSample(opcode, decodeOpcodeArg(byteDataStream, opcode));
+                            if (lastSample == null) {
+                                lastSample = newSample;
+                            } else if (lastSample instanceof RepeatSample) {
+                                final RepeatSample repeatSample = (RepeatSample) lastSample;
+                                final ScalarSample repeatedScalarSample = repeatSample.getSampleRepeated();
+                                if (newSample.equals(repeatedScalarSample)) {
+                                    repeatSample.incrementRepeatCount();
+                                } else {
+                                    encodeSample(dataStream, lastSample);
+                                    lastSample = newSample;
+                                }
+                            } else if (lastSample.equals(newSample)) {
+                                lastSample = new RepeatSample(2, newSample);
+                            } else {
+                                encodeSample(dataStream, lastSample);
+                                lastSample = newSample;
+                            }
+                    }
+                }
+            }
+            if (lastSample != null) {
+                encodeSample(dataStream, lastSample);
+            }
+            dataStream.flush();
+            return outputStream.toByteArray();
+        } catch (Exception e) {
+            log.error("In combineSampleBytes(), exception combining sample byte arrays", e);
+            return new byte[0];
+        }
+    }
+
+    /**
+     * This invokes the processor on the values in the timeline bytes.
+     *
+     * @param chunk     the timeline chuck to scan
+     * @param processor the callback to which values value counts are passed to be processed.
+     * @throws java.io.IOException
+     */
+    @Override
+    public void scan(final TimelineChunk chunk, final SampleProcessor processor) throws IOException {
+        //System.out.printf("Decoded: %s\n", new String(Hex.encodeHex(bytes)));
+        scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), chunk.getTimeBytesAndSampleBytes().getTimeBytes(), chunk.getSampleCount(), processor);
+    }
+
+    @Override
+    public void scan(final byte[] samples, final byte[] times, final int sampleCount, final SampleProcessor processor) throws IOException {
+        final ByteArrayInputStream byteStream = new ByteArrayInputStream(samples);
+        final DataInputStream inputStream = new DataInputStream(byteStream);
+        final TimelineCursor timeCursor = new DefaultTimelineCursor(times, sampleCount);
+        int sampleNumber = 0;
+        while (true) {
+            final int opcodeByte;
+            opcodeByte = inputStream.read();
+            if (opcodeByte == -1) {
+                return; // At "eof"
+            }
+            final SampleOpcode opcode = SampleOpcode.getOpcodeFromIndex(opcodeByte);
+            switch (opcode) {
+                case REPEAT_BYTE:
+                case REPEAT_SHORT:
+                    final int repeatCount = opcode == SampleOpcode.REPEAT_BYTE ? inputStream.readUnsignedByte() : inputStream.readUnsignedShort();
+                    final SampleOpcode repeatedOpcode = SampleOpcode.getOpcodeFromIndex(inputStream.read());
+                    final Object value = decodeScalarValue(inputStream, repeatedOpcode);
+                    final SampleOpcode replacementOpcode = repeatedOpcode.getReplacement();
+                    processor.processSamples(timeCursor, repeatCount, replacementOpcode, value);
+                    sampleNumber += repeatCount;
+                    timeCursor.skipToSampleNumber(sampleNumber);
+                    break;
+                default:
+                    processor.processSamples(timeCursor, 1, opcode.getReplacement(), decodeScalarValue(inputStream, opcode));
+                    break;
+            }
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java
new file mode 100644
index 0000000..62f0a3a
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/EncodedBytesAndSampleCount.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.util.Arrays;
+
+public class EncodedBytesAndSampleCount {
+
+    private final byte[] encodedBytes;
+    private final int sampleCount;
+
+    public EncodedBytesAndSampleCount(final byte[] encodedBytes, final int sampleCount) {
+        this.encodedBytes = encodedBytes;
+        this.sampleCount = sampleCount;
+    }
+
+    public byte[] getEncodedBytes() {
+        return encodedBytes;
+    }
+
+    public int getSampleCount() {
+        return sampleCount;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("EncodedBytesAndSampleCount");
+        sb.append("{encodedBytes=").append(encodedBytes == null ? "null" : "");
+        for (int i = 0; encodedBytes != null && i < encodedBytes.length; ++i) {
+            sb.append(i == 0 ? "" : ", ").append(encodedBytes[i]);
+        }
+        sb.append(", sampleCount=").append(sampleCount);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final EncodedBytesAndSampleCount that = (EncodedBytesAndSampleCount) o;
+
+        if (sampleCount != that.sampleCount) {
+            return false;
+        }
+        if (!Arrays.equals(encodedBytes, that.encodedBytes)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = encodedBytes != null ? Arrays.hashCode(encodedBytes) : 0;
+        result = 31 * result + sampleCount;
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java
new file mode 100644
index 0000000..50a1621
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleAccumulator.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.samples.NullSample;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * Accumulator of samples. Samples are compressed using a SampleCoder.
+ */
+public class SampleAccumulator {
+
+    private static final Logger log = LoggerFactory.getLogger(SampleAccumulator.class);
+    private static final int DEFAULT_CHUNK_BYTE_ARRAY_SIZE = 100;
+
+    private ByteArrayOutputStream byteStream;
+    private DataOutputStream outputStream;
+    private int sampleCount;
+    private SampleBase lastSample;
+    protected final SampleCoder sampleCoder;
+
+    public SampleAccumulator(final SampleCoder sampleCoder) {
+        this.sampleCoder = sampleCoder;
+        reset();
+    }
+
+    public SampleAccumulator(final byte[] bytes, final SampleBase lastSample, final int sampleCount, final SampleCoder sampleCoder) throws IOException {
+        reset();
+        this.byteStream.write(bytes);
+        this.lastSample = lastSample;
+        this.sampleCount = sampleCount;
+        this.sampleCoder = sampleCoder;
+    }
+
+    public void addSampleList(final List<ScalarSample> samples) {
+        for (final ScalarSample sample : samples) {
+            addSample(sample);
+        }
+    }
+
+    public synchronized void addSample(final ScalarSample sample) {
+        if (lastSample == null) {
+            lastSample = sample;
+        } else {
+            final SampleOpcode lastOpcode = lastSample.getOpcode();
+            final SampleOpcode sampleOpcode = sample.getOpcode();
+            if (lastSample instanceof RepeatSample) {
+                final RepeatSample repeatSample = (RepeatSample) lastSample;
+                final ScalarSample sampleRepeated = repeatSample.getSampleRepeated();
+                if (sampleRepeated.getOpcode() == sampleOpcode &&
+                        (sampleOpcode.getNoArgs() || ScalarSample.sameSampleValues(sampleRepeated.getSampleValue(), sample.getSampleValue())) &&
+                        repeatSample.getRepeatCount() < RepeatSample.MAX_SHORT_REPEAT_COUNT) {
+                    // We can just increment the count in the repeat instance
+                    repeatSample.incrementRepeatCount();
+                } else {
+                    // A non-matching repeat - just add it
+                    addLastSample();
+                    lastSample = sample;
+                }
+            } else {
+                final ScalarSample lastScalarSample = (ScalarSample) lastSample;
+                if (sampleOpcode == lastOpcode &&
+                        (sampleOpcode.getNoArgs() || ScalarSample.sameSampleValues(sample.getSampleValue(), lastScalarSample.getSampleValue()))) {
+                    // Replace lastSample with repeat group
+                    lastSample = new RepeatSample(2, lastScalarSample);
+                } else {
+                    addLastSample();
+                    lastSample = sample;
+                }
+            }
+        }
+        // In all cases, we got 1 more sample
+        sampleCount++;
+    }
+
+    public int getSampleCount() {
+        return sampleCount;
+    }
+
+    protected ByteArrayOutputStream getByteStream() {
+        return byteStream;
+    }
+
+    protected SampleBase getLastSample() {
+        return lastSample;
+    }
+
+    /**
+     * The log scanner can safely call this method, and know that the byte
+     * array will always end in a complete sample
+     *
+     * @return an instance containing the bytes and the counts of samples
+     */
+    public synchronized EncodedBytesAndSampleCount getEncodedSamples() {
+        if (lastSample != null) {
+            sampleCoder.encodeSample(outputStream, lastSample);
+            lastSample = null;
+        }
+        try {
+            outputStream.flush();
+            return new EncodedBytesAndSampleCount(byteStream.toByteArray(), sampleCount);
+        } catch (IOException e) {
+            log.error("In getEncodedSamples, IOException flushing outputStream", e);
+            // Do no harm - - this at least won't corrupt the encoding
+            return new EncodedBytesAndSampleCount(new byte[0], 0);
+        }
+    }
+
+    private synchronized void addLastSample() {
+        if (lastSample != null) {
+            sampleCoder.encodeSample(outputStream, lastSample);
+            lastSample = null;
+        }
+    }
+
+    public synchronized void reset() {
+        byteStream = new ByteArrayOutputStream(DEFAULT_CHUNK_BYTE_ARRAY_SIZE);
+        outputStream = new DataOutputStream(byteStream);
+        lastSample = null;
+        sampleCount = 0;
+    }
+
+    public synchronized void addPlaceholder(final int repeatCount) {
+        if (repeatCount > 0) {
+            addLastSample();
+            lastSample = new RepeatSample<Void>(repeatCount, new NullSample());
+            sampleCount += repeatCount;
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java
new file mode 100644
index 0000000..b218f8e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleCoder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * Samples compressor and decompressor
+ */
+public interface SampleCoder {
+
+    public byte[] compressSamples(final List<ScalarSample> samples);
+
+    public List<ScalarSample> decompressSamples(final byte[] sampleBytes) throws IOException;
+
+    /**
+     * This method writes the binary encoding of the sample to the outputStream.  This encoding
+     * is the form saved in the db and scanned when read from the db.
+     *
+     * @param outputStream the stream to which bytes should be written
+     * @param sample       the sample to be written
+     */
+    public void encodeSample(final DataOutputStream outputStream, final SampleBase sample);
+
+    /**
+     * Output the scalar value into the output stream
+     *
+     * @param outputStream the stream to which bytes should be written
+     * @param value        the sample value, interpreted according to the opcode
+     */
+    public void encodeScalarValue(final DataOutputStream outputStream, final SampleOpcode opcode, final Object value);
+
+    /**
+     * This routine returns a ScalarSample that may have a smaller representation than the
+     * ScalarSample argument.  In particular, if tries hard to choose the most compact
+     * representation of double-precision values.
+     *
+     * @param sample A ScalarSample to be compressed
+     * @return Either the same ScalarSample is that input, for for some cases of opcode DOUBLE,
+     *         a more compact ScalarSample which when processed returns a double value.
+     */
+    public ScalarSample compressSample(final ScalarSample sample);
+
+    public Object decodeScalarValue(final DataInputStream inputStream, final SampleOpcode opcode) throws IOException;
+
+    public double getMaxFractionError();
+
+    public byte[] combineSampleBytes(final List<byte[]> sampleBytesList);
+
+    public void scan(final TimelineChunk chunk, final SampleProcessor processor) throws IOException;
+
+    public void scan(final byte[] samples, final byte[] times, final int sampleCount, final SampleProcessor processor) throws IOException;
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java
new file mode 100644
index 0000000..077a107
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/SampleProcessor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public interface SampleProcessor {
+
+    /**
+     * Process sampleCount sequential samples with identical values.  sampleCount will usually be 1,
+     * but may be larger than 1.  Implementors may just loop processing identical values, but some
+     * implementations may optimize adding a bunch of repeated values
+     *
+     * @param timeCursor  a TimeCursor object from which times can be found.
+     * @param sampleCount the count of sequential, identical values
+     * @param opcode      the opcode of the sample value, which may not be a REPEAT opcode
+     * @param value       the value of this kind of sample over the count of samples
+     */
+    public void processSamples(final TimelineCursor timeCursor,
+                               final int sampleCount,
+                               final SampleOpcode opcode,
+                               final Object value);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java
new file mode 100644
index 0000000..69ab1b5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkAccumulator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleBase;
+
+/**
+ * This class represents a sequence of values for a single attribute,
+ * e.g., "TP99 Response Time", for one source and one specific time range,
+ * as the object is being accumulated.  It is not used to represent
+ * past timeline sequences; they are held in TimelineChunk objects.
+ * <p/>
+ * It accumulates samples in a byte array object. Readers can call
+ * getEncodedSamples() at any time to get the latest data.
+ */
+public class TimelineChunkAccumulator extends SampleAccumulator {
+
+    private static final Logger log = LoggerFactory.getLogger(TimelineChunkAccumulator.class);
+    private final int sourceId;
+    private final int metricId;
+
+    public TimelineChunkAccumulator(final int sourceId, final int metricId, final SampleCoder sampleCoder) {
+        super(sampleCoder);
+        this.sourceId = sourceId;
+        this.metricId = metricId;
+    }
+
+    private TimelineChunkAccumulator(final int sourceId, final int metricId, final byte[] bytes, final SampleBase lastSample, final int sampleCount, final SampleCoder sampleCoder) throws IOException {
+        super(bytes, lastSample, sampleCount, sampleCoder);
+        this.sourceId = sourceId;
+        this.metricId = metricId;
+    }
+
+    public TimelineChunkAccumulator deepCopy() throws IOException {
+        return new TimelineChunkAccumulator(sourceId, metricId, getByteStream().toByteArray(), getLastSample(), getSampleCount(), sampleCoder);
+    }
+
+    /**
+     * This method grabs the current encoded form, and resets the accumulator
+     */
+    public synchronized TimelineChunk extractTimelineChunkAndReset(final DateTime startTime, final DateTime endTime, final byte[] timeBytes) {
+        // Extract the chunk
+        final byte[] sampleBytes = getEncodedSamples().getEncodedBytes();
+        log.debug("Creating TimelineChunk for metricId {}, sampleCount {}", metricId, getSampleCount());
+        final TimelineChunk chunk = new TimelineChunk(0, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, getSampleCount());
+
+        // Reset this current accumulator
+        reset();
+
+        return chunk;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    public int getMetricId() {
+        return metricId;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java
new file mode 100644
index 0000000..9b3ae3e
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimelineChunkDecoded.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TimelineChunkDecoded {
+
+    private static final Logger log = LoggerFactory.getLogger(TimelineChunkDecoded.class);
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    private final TimelineChunk chunk;
+    private final SampleCoder sampleCoder;
+
+    public TimelineChunkDecoded(final TimelineChunk chunk, final SampleCoder sampleCoder) {
+        this.chunk = chunk;
+        this.sampleCoder = sampleCoder;
+    }
+
+    @JsonValue
+    @Override
+    public String toString() {
+        try {
+            final ByteArrayOutputStream out = new ByteArrayOutputStream();
+            final JsonGenerator generator = objectMapper.getJsonFactory().createJsonGenerator(out);
+            generator.writeStartObject();
+
+            generator.writeFieldName("metric");
+            generator.writeNumber(chunk.getMetricId());
+
+            generator.writeFieldName("decodedSamples");
+            generator.writeString(getDecodedSamples());
+
+            generator.writeEndObject();
+            generator.close();
+            return out.toString();
+        } catch (IOException e) {
+            log.error("IOException in toString()", e);
+        }
+
+        return null;
+    }
+
+    private String getDecodedSamples() throws IOException {
+        final DecodedSampleOutputProcessor processor = new DecodedSampleOutputProcessor();
+        sampleCoder.scan(chunk, processor);
+        return processor.getDecodedSamples();
+    }
+
+    private static final class DecodedSampleOutputProcessor implements SampleProcessor {
+
+        final StringBuilder builder = new StringBuilder();
+
+        @Override
+        public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+            if (builder.length() > 0) {
+                builder.append(", ");
+            }
+            final DateTime timestamp = timeCursor.getNextTime();
+            builder.append("at ").append(timestamp.toString("yyyy-MM-dd HH:mm:ss")).append(" ");
+            if (sampleCount > 1) {
+                builder.append(sampleCount).append(" of ");
+            }
+            builder.append(opcode.name().toLowerCase());
+            switch (opcode) {
+                case NULL:
+                case DOUBLE_ZERO:
+                case INT_ZERO:
+                    break;
+                default:
+                    builder.append("(").append(String.valueOf(value)).append(")");
+                    break;
+            }
+        }
+
+        public String getDecodedSamples() {
+            return builder.toString();
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java
new file mode 100644
index 0000000..f0ffc32
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimeRangeSampleProcessor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public abstract class TimeRangeSampleProcessor implements SampleProcessor {
+
+    private final DateTime startTime;  // Inclusive
+    private final DateTime endTime;    // Inclusive
+
+    public TimeRangeSampleProcessor(final DateTime startTime, final DateTime endTime) {
+        this.startTime = startTime;
+        this.endTime = endTime;
+    }
+
+    /**
+     * Process sampleCount sequential samples with identical values.  sampleCount will usually be 1,
+     * but may be larger than 1.  Implementors may just loop processing identical values, but some
+     * implementations may optimize adding a bunch of repeated values
+     *
+     * @param timeCursor  a TimeCursor instance, which supplies successive int UNIX times
+     * @param sampleCount the count of sequential, identical values
+     * @param opcode      the opcode of the sample value, which may not be a REPEAT opcode
+     * @param value       the value of this kind of sample over the sampleCount samples
+     */
+    @Override
+    public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+        for (int i = 0; i < sampleCount; i++) {
+            // Check if the sample is in the right time range
+            final DateTime sampleTime = timeCursor.getNextTime();
+            if ((startTime == null || !sampleTime.isBefore(startTime)) && ((endTime == null || !sampleTime.isAfter(endTime)))) {
+                processOneSample(sampleTime, opcode, value);
+            }
+        }
+    }
+
+    public abstract void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value);
+
+    public DateTime getStartTime() {
+        return startTime;
+    }
+
+    public DateTime getEndTime() {
+        return endTime;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java
new file mode 100644
index 0000000..5d2a179
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/codec/TimesAndSamplesCoder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import com.ning.billing.usage.timeline.chunks.TimeBytesAndSampleBytes;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class TimesAndSamplesCoder {
+
+    public static int getSizeOfTimeBytes(final byte[] timesAndSamples) {
+        final DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(timesAndSamples));
+        try {
+            return inputStream.readInt();
+        } catch (IOException e) {
+            throw new IllegalStateException(String.format("Exception reading timeByteCount in TimelineChunkMapper.map() for timesAndSamples %s",
+                                                          new String(Hex.encodeHex(timesAndSamples))), e);
+        }
+    }
+
+    public static int getEncodedLength(final TimelineChunk chunk) {
+        return 4 + chunk.getTimeBytesAndSampleBytes().getTimeBytes().length +
+                chunk.getTimeBytesAndSampleBytes().getSampleBytes().length;
+    }
+
+    public static byte[] getTimeBytes(final byte[] timesAndSamples) {
+        final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+        return Arrays.copyOfRange(timesAndSamples, 4, 4 + timeByteCount);
+    }
+
+    public static byte[] getSampleBytes(final byte[] timesAndSamples) {
+        final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+        return Arrays.copyOfRange(timesAndSamples, 4 + timeByteCount, timesAndSamples.length);
+    }
+
+    public static TimeBytesAndSampleBytes getTimesBytesAndSampleBytes(final byte[] timesAndSamples) {
+        final int timeByteCount = getSizeOfTimeBytes(timesAndSamples);
+        final byte[] timeBytes = Arrays.copyOfRange(timesAndSamples, 4, 4 + timeByteCount);
+        final byte[] sampleBytes = Arrays.copyOfRange(timesAndSamples, 4 + timeByteCount, timesAndSamples.length);
+        return new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+    }
+
+    public static byte[] combineTimesAndSamples(final byte[] times, final byte[] samples) {
+        final int totalSamplesSize = 4 + times.length + samples.length;
+        final ByteArrayOutputStream baStream = new ByteArrayOutputStream(totalSamplesSize);
+        final DataOutputStream outputStream = new DataOutputStream(baStream);
+        try {
+            outputStream.writeInt(times.length);
+            outputStream.write(times);
+            outputStream.write(samples);
+            outputStream.flush();
+            return baStream.toByteArray();
+        } catch (IOException e) {
+            throw new IllegalStateException(String.format("Exception reading timeByteCount in TimelineChunkMapper.map() for times %s, samples %s",
+                                                          new String(Hex.encodeHex(times)), new String(Hex.encodeHex(samples))), e);
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java
new file mode 100644
index 0000000..879e944
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVConsumer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.consumer;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.filter.DecimatingSampleFilter;
+
+public class CSVConsumer {
+
+    private CSVConsumer() {
+    }
+
+    public static String getSamplesAsCSV(final SampleCoder sampleCoder, final TimelineChunk chunk, final DecimatingSampleFilter rangeSampleProcessor) throws IOException {
+        sampleCoder.scan(chunk, rangeSampleProcessor);
+        return rangeSampleProcessor.getSampleConsumer().toString();
+    }
+
+    public static String getSamplesAsCSV(final SampleCoder sampleCoder, final TimelineChunk chunk, @Nullable final DateTime startTime, @Nullable final DateTime endTime) throws IOException {
+        final CSVOutputProcessor processor = new CSVOutputProcessor(startTime, endTime);
+        sampleCoder.scan(chunk, processor);
+        return processor.toString();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java
new file mode 100644
index 0000000..6e5aa11
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVOutputProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.consumer;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.codec.TimeRangeSampleProcessor;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public class CSVOutputProcessor extends TimeRangeSampleProcessor {
+
+    private final SampleConsumer delegate = new CSVSampleConsumer();
+    private int sampleNumber = 0;
+
+    public CSVOutputProcessor(@Nullable final DateTime startTime, @Nullable final DateTime endTime) {
+        super(startTime, endTime);
+    }
+
+    @Override
+    public void processOneSample(final DateTime sampleTimestamp, final SampleOpcode opcode, final Object value) {
+        delegate.consumeSample(sampleNumber, opcode, value, sampleTimestamp);
+        sampleNumber++;
+    }
+
+    @Override
+    public String toString() {
+        return delegate.toString();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java
new file mode 100644
index 0000000..cde8725
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/CSVSampleConsumer.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.consumer;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+public class CSVSampleConsumer implements SampleConsumer {
+
+    private final StringBuilder builder = new StringBuilder();
+    // Use our private counter because of the decimating filter
+    private int builderSampleNumber = 0;
+
+    @Override
+    public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+        if (time != null) {
+            final String valueString = value == null ? "0" : value.toString();
+            if (builderSampleNumber > 0) {
+                builder.append(",");
+            }
+
+            builder.append(DateTimeUtils.unixSeconds(time))
+                   .append(",")
+                   .append(valueString);
+            builderSampleNumber++;
+        }
+    }
+
+    @Override
+    public synchronized String toString() {
+        final String value = builder.toString();
+        // Allow for re-use
+        builder.setLength(0);
+        builderSampleNumber = 0;
+        return value;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java
new file mode 100644
index 0000000..f6a04c3
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/SampleConsumer.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.consumer;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public interface SampleConsumer {
+
+    public void consumeSample(int sampleNumber, SampleOpcode opcode, Object value, DateTime time);
+}
+
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java
new file mode 100644
index 0000000..1c403da
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/consumer/TimelineChunkConsumer.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.consumer;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+
+public interface TimelineChunkConsumer {
+
+    public void processTimelineChunk(TimelineChunk chunk);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java
new file mode 100644
index 0000000..ccf26a2
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimatingSampleFilter.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.filter;
+
+import org.joda.time.DateTime;
+import org.skife.config.TimeSpan;
+
+import com.ning.billing.usage.timeline.codec.TimeRangeSampleProcessor;
+import com.ning.billing.usage.timeline.consumer.SampleConsumer;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+/**
+ * This SampleProcessor interpolates a stream of sample values to such that the
+ * number of outputs sent to the SampleConsumer is outputCount, which is less
+ * than sampleCount. It works by keeping a history of scanned samples
+ * representing at least one output sample, and makes a choice of what value to
+ * output from those scanned samples:
+ * <p/>
+ * The rules for sample generation are:
+ * <ul>
+ * <li>No averaging - - the sample returned is _always_ one of the scanned
+ * samples</li>
+ * <li>The output sample is always either the largest or the smallest of the
+ * scanned sample values</li>
+ * <li>Whether it is the largest or smallest depends on the "trend" of the
+ * samples:
+ * <ul>
+ * <li>If they are generally high-to-low then we output the low value.</li>
+ * <li>If they are generally low-to-high then we output the high value.</li>
+ * </ul>
+ * </ul>
+ * <p/>
+ * The rationale for these rules is the most interesting information is the
+ * peaks and valleys of measurements, and averaging is bad because it destroys
+ * peaks and valleys. A consequence of these rules is that quantities that
+ * bounce around a lot will generate graphs that are a solid band between the
+ * min and max values. But that's really an accurate reflection of the state. To
+ * get more information, you have to look at shorter time intervals. The class
+ * tries hard to make good choices amount
+ * <p/>
+ * Of course this sort of crude averaging isn't perfect, but at least it doesn't
+ * destroy peaks and valleys.
+ * <p/>
+ * TODO: Figure out if the time passed to SampleConsumer should be the time
+ * of the sample or the midpoint of the times between first and last sample.
+ */
+public class DecimatingSampleFilter extends TimeRangeSampleProcessor {
+
+    private final int outputCount;
+    private final SampleConsumer sampleConsumer;
+    private final TimeSpan pollingInterval;
+    private final DecimationMode decimationMode;
+    private double samplesPerOutput;
+    private double outputsPerSample;
+    private int ceilSamplesPerOutput;
+    private SampleState[] filterHistory;
+    private boolean initialized = false;
+
+    private double runningSum = 0.0;
+    private int sampleNumber = 0;
+
+    /**
+     * Build a DecimatingSampleFilter on which you call processSamples()
+     *
+     * @param startTime       The start time we're considering values, or null, meaning all time
+     * @param endTime         The end time we're considering values, or null, meaning all time
+     * @param outputCount     The number of samples to generate
+     * @param sampleCount     The number of samples to be scanned.  sampleCount must be >= outputCount
+     * @param pollingInterval The polling interval, used to compute sample counts assuming no gaps
+     * @param decimationMode  The decimation mode determines how samples will be combined to crate an output point.
+     * @param sampleConsumer  The implementor of the SampleConsumer interface
+     */
+    public DecimatingSampleFilter(final DateTime startTime, final DateTime endTime, final int outputCount, final int sampleCount,
+                                  final TimeSpan pollingInterval, final DecimationMode decimationMode, final SampleConsumer sampleConsumer) {
+        super(startTime, endTime);
+        if (outputCount <= 0 || sampleCount <= 0 || outputCount > sampleCount) {
+            throw new IllegalArgumentException(String.format("In DecimatingSampleFilter, outputCount is %d but sampleCount is %d", outputCount, sampleCount));
+        }
+        this.outputCount = outputCount;
+        this.pollingInterval = pollingInterval;
+        this.decimationMode = decimationMode;
+        this.sampleConsumer = sampleConsumer;
+        initializeFilterHistory(sampleCount);
+    }
+
+    /**
+     * This form of the constructor delays initialization til we get the first sample
+     *
+     * @param startTime       The start time we're considering values, or null, meaning all time
+     * @param endTime         The end time we're considering values, or null, meaning all time
+     * @param outputCount     The number of samples to generate
+     * @param pollingInterval The polling interval, used to compute sample counts assuming no gaps
+     * @param decimationMode  The decimation mode determines how samples will be combined to crate an output point.
+     * @param sampleConsumer  The implementor of the SampleConsumer interface
+     */
+    public DecimatingSampleFilter(final DateTime startTime, final DateTime endTime, final int outputCount, final TimeSpan pollingInterval,
+                                  final DecimationMode decimationMode, final SampleConsumer sampleConsumer) {
+        super(startTime, endTime);
+        this.outputCount = outputCount;
+        this.pollingInterval = pollingInterval;
+        this.decimationMode = decimationMode;
+        this.sampleConsumer = sampleConsumer;
+    }
+
+    private void initializeFilterHistory(final int sampleCount) {
+        if (outputCount <= 0 || sampleCount <= 0 || outputCount > sampleCount) {
+            throw new IllegalArgumentException(String.format("In DecimatingSampleFilter.initialize(), outputCount is %d but sampleCount is %d", outputCount, sampleCount));
+        }
+        this.samplesPerOutput = (double) sampleCount / (double) outputCount;
+        this.outputsPerSample = 1.0 / this.samplesPerOutput;
+        ceilSamplesPerOutput = (int) Math.ceil(samplesPerOutput);
+        filterHistory = new SampleState[ceilSamplesPerOutput];
+        initialized = true;
+    }
+
+    @Override
+    public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+        if (!initialized) {
+            // Estimate the sampleCount, assuming that there are no gaps
+            final long adjustedEndMillis = Math.min(getEndTime().getMillis(), System.currentTimeMillis());
+            final long millisTilEnd = adjustedEndMillis - time.getMillis();
+            final int sampleCount = Math.max(outputCount, (int) (millisTilEnd / pollingInterval.getMillis()));
+            initializeFilterHistory(sampleCount);
+        }
+        sampleNumber++;
+        final SampleState sampleState = new SampleState(opcode, value, ScalarSample.getDoubleValue(opcode, value), time);
+        final int historyIndex = sampleNumber % filterHistory.length;
+        filterHistory[historyIndex] = sampleState;
+        runningSum += outputsPerSample;
+        if (runningSum >= 1.0) {
+            runningSum -= 1.0;
+            if (opcode == SampleOpcode.STRING) {
+                // We don't have interpolation, so just output
+                // this one
+                sampleConsumer.consumeSample(sampleNumber, opcode, value, time);
+            } else {
+                // Time to output a sample - compare the sum of the first samples with the
+                // sum of the last samples making up the output, choosing the lowest value if
+                // if the first samples are larger, and the highest value if the last samples
+                // are larger
+                final int samplesInAverage = ceilSamplesPerOutput > 5 ? ceilSamplesPerOutput * 2 / 3 : Math.max(1, ceilSamplesPerOutput - 1);
+                final int samplesLeftOut = ceilSamplesPerOutput - samplesInAverage;
+                double max = Double.MIN_VALUE;
+                int maxIndex = 0;
+                int minIndex = 0;
+                double min = Double.MAX_VALUE;
+                double sum = 0.0;
+                double firstSum = 0.0;
+                double lastSum = 0.0;
+                for (int i = 0; i < ceilSamplesPerOutput; i++) {
+                    final int index = (sampleNumber + ceilSamplesPerOutput - i) % ceilSamplesPerOutput;
+                    final SampleState sample = filterHistory[index];
+                    if (sample != null) {
+                        final double doubleValue = sample.getDoubleValue();
+                        sum += doubleValue;
+                        if (doubleValue > max) {
+                            max = doubleValue;
+                            maxIndex = index;
+                        }
+                        if (doubleValue < min) {
+                            min = doubleValue;
+                            minIndex = index;
+                        }
+                        if (i < samplesInAverage) {
+                            lastSum += doubleValue;
+                        }
+                        if (i >= samplesLeftOut) {
+                            firstSum += doubleValue;
+                        }
+                    }
+                }
+                final SampleState firstSample = filterHistory[(sampleNumber + ceilSamplesPerOutput - (ceilSamplesPerOutput - 1)) % ceilSamplesPerOutput];
+                final SampleState lastSample = filterHistory[sampleNumber % ceilSamplesPerOutput];
+                final DateTime centerTime = firstSample != null ? new DateTime((firstSample.getTime().getMillis() + lastSample.getTime().getMillis()) >> 1) : lastSample.getTime();
+                switch (decimationMode) {
+                    case PEAK_PICK:
+                        if (firstSum > lastSum) {
+                            // The sample window is generally down with time - - pick the minimum
+                            final SampleState minSample = filterHistory[minIndex];
+                            sampleConsumer.consumeSample(sampleNumber, minSample.getSampleOpcode(), minSample.getValue(), centerTime);
+                        } else {
+                            // The sample window is generally up with time - - pick the maximum
+                            final SampleState maxSample = filterHistory[maxIndex];
+                            sampleConsumer.consumeSample(sampleNumber, maxSample.getSampleOpcode(), maxSample.getValue(), centerTime);
+                        }
+                        break;
+                    case AVERAGE:
+                        final double average = sum / ceilSamplesPerOutput;
+                        sampleConsumer.consumeSample(minIndex, SampleOpcode.DOUBLE, average, centerTime);
+                        break;
+                    default:
+                        throw new IllegalStateException(String.format("The decimation filter mode %s is not recognized", decimationMode));
+                }
+            }
+        }
+    }
+
+    public SampleConsumer getSampleConsumer() {
+        return sampleConsumer;
+    }
+
+    private static class SampleState {
+
+        private final SampleOpcode sampleOpcode;
+        private final Object value;
+        private final double doubleValue;
+        private final DateTime time;
+
+        public SampleState(final SampleOpcode sampleOpcode, final Object value, final double doubleValue, final DateTime time) {
+            this.sampleOpcode = sampleOpcode;
+            this.value = value;
+            this.doubleValue = doubleValue;
+            this.time = time;
+        }
+
+        public SampleOpcode getSampleOpcode() {
+            return sampleOpcode;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public double getDoubleValue() {
+            return doubleValue;
+        }
+
+        public DateTime getTime() {
+            return time;
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java
new file mode 100644
index 0000000..0523bd7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/filter/DecimationMode.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.filter;
+
+public enum DecimationMode {
+    PEAK_PICK,
+    AVERAGE;
+
+    public static DecimationMode fromString(final String modeString) {
+        for (final DecimationMode decimationMode : DecimationMode.values()) {
+            if (decimationMode.name().equalsIgnoreCase(modeString)) {
+                return decimationMode;
+            }
+        }
+        return null;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java
new file mode 100644
index 0000000..760eaf1
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndId.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.metrics;
+
+public class MetricAndId {
+
+    private final String metric;
+    private final int metricId;
+
+    public MetricAndId(final String metric, final int metricId) {
+        this.metric = metric;
+        this.metricId = metricId;
+    }
+
+    public String getMetric() {
+        return metric;
+    }
+
+    public int getMetricId() {
+        return metricId;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("MetricAndId");
+        sb.append("{metric='").append(metric).append('\'');
+        sb.append(", metricId=").append(metricId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final MetricAndId that = (MetricAndId) o;
+
+        if (metricId != that.metricId) {
+            return false;
+        }
+        if (metric != null ? !metric.equals(that.metric) : that.metric != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = metric != null ? metric.hashCode() : 0;
+        result = 31 * result + metricId;
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java
new file mode 100644
index 0000000..2cba491
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/MetricAndIdMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.metrics;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class MetricAndIdMapper implements ResultSetMapper<MetricAndId> {
+
+    @Override
+    public MetricAndId map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        return new MetricAndId(r.getString("sample_kind"), r.getInt("sample_kind_id"));
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java
new file mode 100644
index 0000000..acb0cb7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/metrics/SamplesForMetricAndSource.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.metrics;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SamplesForMetricAndSource {
+
+    @JsonProperty
+    private final String sourceName;
+
+    @JsonProperty
+    private final String eventCategory;
+
+    @JsonProperty
+    private final String metric;
+
+    @JsonProperty
+    private final String samples;
+
+    @JsonCreator
+    public SamplesForMetricAndSource(@JsonProperty("sourceName") final String sourceName, @JsonProperty("eventCategory") final String eventCategory,
+                                     @JsonProperty("metric") final String metric, @JsonProperty("samples") final String samples) {
+        this.sourceName = sourceName;
+        this.eventCategory = eventCategory;
+        this.metric = metric;
+        this.samples = samples;
+    }
+
+    public String getSourceName() {
+        return sourceName;
+    }
+
+    public String getEventCategory() {
+        return eventCategory;
+    }
+
+    public String getMetric() {
+        return metric;
+    }
+
+    public String getSamples() {
+        return samples;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SamplesForMetricAndSource");
+        sb.append("{eventCategory='").append(eventCategory).append('\'');
+        sb.append(", sourceName='").append(sourceName).append('\'');
+        sb.append(", metric='").append(metric).append('\'');
+        sb.append(", samples='").append(samples).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final SamplesForMetricAndSource that = (SamplesForMetricAndSource) o;
+
+        if (!eventCategory.equals(that.eventCategory)) {
+            return false;
+        }
+        if (!sourceName.equals(that.sourceName)) {
+            return false;
+        }
+        if (!metric.equals(that.metric)) {
+            return false;
+        }
+        if (!samples.equals(that.samples)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = sourceName.hashCode();
+        result = 31 * result + eventCategory.hashCode();
+        result = 31 * result + metric.hashCode();
+        result = 31 * result + samples.hashCode();
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java b/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java
new file mode 100644
index 0000000..5337e77
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/PendingChunkMap.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline;
+
+import java.util.Map;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+
+public class PendingChunkMap {
+
+    private final TimelineSourceEventAccumulator accumulator;
+    private final long pendingChunkMapId;
+    private final Map<Integer, TimelineChunk> chunkMap;
+
+    public PendingChunkMap(final TimelineSourceEventAccumulator accumulator, final long pendingChunkMapId, final Map<Integer, TimelineChunk> chunkMap) {
+        this.accumulator = accumulator;
+        this.pendingChunkMapId = pendingChunkMapId;
+        this.chunkMap = chunkMap;
+    }
+
+    public TimelineSourceEventAccumulator getAccumulator() {
+        return accumulator;
+    }
+
+    public long getPendingChunkMapId() {
+        return pendingChunkMapId;
+    }
+
+    public Map<Integer, TimelineChunk> getChunkMap() {
+        return chunkMap;
+    }
+
+    public int getChunkCount() {
+        return chunkMap.size();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java
new file mode 100644
index 0000000..e6d74d7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/CachingTimelineDao.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableList;
+
+public class CachingTimelineDao implements TimelineDao {
+
+    private static final Logger log = LoggerFactory.getLogger(CachingTimelineDao.class);
+
+    private final BiMap<Integer, String> sourcesCache;
+    private final Map<Integer, Set<Integer>> sourceIdsMetricIdsCache;
+    private final BiMap<Integer, CategoryIdAndMetric> metricsCache;
+    private final BiMap<Integer, String> eventCategoriesCache;
+
+    private final TimelineDao delegate;
+
+    public CachingTimelineDao(final TimelineDao delegate) {
+        this.delegate = delegate;
+        sourcesCache = delegate.getSources();
+        metricsCache = delegate.getMetrics();
+        eventCategoriesCache = delegate.getEventCategories();
+        sourceIdsMetricIdsCache = new HashMap<Integer, Set<Integer>>();
+        for (final SourceIdAndMetricId both : delegate.getMetricIdsForAllSources()) {
+            final int sourceId = both.getSourceId();
+            final int metricId = both.getMetricId();
+            Set<Integer> metricIds = sourceIdsMetricIdsCache.get(sourceId);
+            if (metricIds == null) {
+                metricIds = new HashSet<Integer>();
+                sourceIdsMetricIdsCache.put(sourceId, metricIds);
+            }
+            metricIds.add(metricId);
+        }
+    }
+
+    @Override
+    public Integer getSourceId(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+        return sourcesCache.inverse().get(source);
+    }
+
+    @Override
+    public String getSource(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return sourcesCache.get(sourceId);
+    }
+
+    @Override
+    public BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getSources();
+    }
+
+    @Override
+    public synchronized int getOrAddSource(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+        Integer sourceId = sourcesCache.inverse().get(source);
+        if (sourceId == null) {
+            sourceId = delegate.getOrAddSource(source);
+            sourcesCache.put(sourceId, source);
+        }
+
+        return sourceId;
+    }
+
+    @Override
+    public Integer getEventCategoryId(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+        return eventCategoriesCache.inverse().get(eventCategory);
+    }
+
+    @Override
+    public String getEventCategory(final Integer eventCategoryId) throws UnableToObtainConnectionException {
+        return eventCategoriesCache.get(eventCategoryId);
+    }
+
+    @Override
+    public BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getEventCategories();
+    }
+
+    @Override
+    public int getOrAddEventCategory(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+        Integer eventCategoryId = eventCategoriesCache.inverse().get(eventCategory);
+        if (eventCategoryId == null) {
+            eventCategoryId = delegate.getOrAddEventCategory(eventCategory);
+            eventCategoriesCache.put(eventCategoryId, eventCategory);
+        }
+        return eventCategoryId;
+    }
+
+    @Override
+    public Integer getMetricId(final int eventCategoryId, final String metric) throws UnableToObtainConnectionException {
+        return metricsCache.inverse().get(new CategoryIdAndMetric(eventCategoryId, metric));
+    }
+
+    @Override
+    public CategoryIdAndMetric getCategoryIdAndMetric(final Integer metricId) throws UnableToObtainConnectionException {
+        return metricsCache.get(metricId);
+    }
+
+    @Override
+    public BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getMetrics();
+    }
+
+    @Override
+    public synchronized int getOrAddMetric(final Integer sourceId, final Integer eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+        final CategoryIdAndMetric categoryIdAndMetric = new CategoryIdAndMetric(eventCategoryId, metric);
+        Integer metricId = metricsCache.inverse().get(categoryIdAndMetric);
+        if (metricId == null) {
+            metricId = delegate.getOrAddMetric(sourceId, eventCategoryId, metric);
+            metricsCache.put(metricId, categoryIdAndMetric);
+        }
+        if (sourceId != null) {
+            Set<Integer> metricIds = sourceIdsMetricIdsCache.get(sourceId);
+            if (metricIds == null) {
+                metricIds = new HashSet<Integer>();
+                sourceIdsMetricIdsCache.put(sourceId, metricIds);
+            }
+            metricIds.add(metricId);
+        }
+        return metricId;
+    }
+
+    @Override
+    public Iterable<Integer> getMetricIdsBySourceId(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return ImmutableList.copyOf(sourceIdsMetricIdsCache.get(sourceId));
+    }
+
+    @Override
+    public Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getMetricIdsForAllSources();
+    }
+
+
+    @Override
+    public Long insertTimelineChunk(final TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.insertTimelineChunk(timelineChunk);
+    }
+
+    @Override
+    public void getSamplesBySourceIdsAndMetricIds(final List<Integer> sourceIds, @Nullable final List<Integer> metricIds,
+                                                  final DateTime startTime, final DateTime endTime, final TimelineChunkConsumer chunkConsumer) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.getSamplesBySourceIdsAndMetricIds(sourceIds, metricIds, startTime, endTime, chunkConsumer);
+    }
+
+    @Override
+    public Integer insertLastStartTimes(final StartTimes startTimes) {
+        return delegate.insertLastStartTimes(startTimes);
+    }
+
+    @Override
+    public StartTimes getLastStartTimes() {
+        return delegate.getLastStartTimes();
+    }
+
+    @Override
+    public void deleteLastStartTimes() {
+        delegate.deleteLastStartTimes();
+    }
+
+    @Override
+    public void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.bulkInsertEventCategories(categoryNames);
+    }
+
+    @Override
+    public void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.bulkInsertSources(sources);
+    }
+
+    @Override
+    public void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds) {
+        delegate.bulkInsertMetrics(categoryAndKinds);
+    }
+
+    @Override
+    public void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList) {
+        delegate.bulkInsertTimelineChunks(timelineChunkList);
+    }
+
+    @Override
+    public void test() throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.test();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java
new file mode 100644
index 0000000..37ba9ae
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/DefaultTimelineDao.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.ResultIterator;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocator;
+import org.skife.jdbi.v2.tweak.HandleCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkMapper;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.inject.Inject;
+
+public class DefaultTimelineDao implements TimelineDao {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultTimelineDao.class);
+    private static final Joiner JOINER = Joiner.on(",");
+
+    private final IDBI dbi;
+    private final TimelineChunkMapper timelineChunkMapper;
+    private final TimelineSqlDao delegate;
+
+    @Inject
+    public DefaultTimelineDao(final IDBI dbi) {
+        this.dbi = dbi;
+        this.timelineChunkMapper = new TimelineChunkMapper();
+        this.delegate = dbi.onDemand(TimelineSqlDao.class);
+    }
+
+    @Override
+    public String getSource(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getSource(sourceId);
+    }
+
+    @Override
+    public Integer getSourceId(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getSourceId(source);
+    }
+
+    @Override
+    public BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException {
+        final HashBiMap<Integer, String> accumulator = HashBiMap.create();
+        for (final Map<String, Object> metric : delegate.getSources()) {
+            accumulator.put(Integer.valueOf(metric.get("source_id").toString()), metric.get("source_name").toString());
+        }
+        return accumulator;
+    }
+
+    @Override
+    public synchronized int getOrAddSource(final String source) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.begin();
+        delegate.addSource(source);
+        final Integer sourceId = delegate.getSourceId(source);
+        delegate.commit();
+
+        return sourceId;
+    }
+
+    @Override
+    public Integer getEventCategoryId(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getEventCategoryId(eventCategory);
+    }
+
+    @Override
+    public String getEventCategory(final Integer eventCategoryId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getEventCategory(eventCategoryId);
+    }
+
+    @Override
+    public BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException {
+        final HashBiMap<Integer, String> accumulator = HashBiMap.create();
+        for (final Map<String, Object> eventCategory : delegate.getEventCategories()) {
+            accumulator.put(Integer.valueOf(eventCategory.get("event_category_id").toString()), eventCategory.get("event_category").toString());
+        }
+        return accumulator;
+    }
+
+    @Override
+    public synchronized int getOrAddEventCategory(final String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.begin();
+        delegate.addEventCategory(eventCategory);
+        final Integer eventCategoryId = delegate.getEventCategoryId(eventCategory);
+        delegate.commit();
+
+        return eventCategoryId;
+    }
+
+    @Override
+    public Integer getMetricId(final int eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getMetricId(eventCategoryId, metric);
+    }
+
+    @Override
+    public CategoryIdAndMetric getCategoryIdAndMetric(final Integer metricId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getEventCategoryIdAndMetric(metricId);
+    }
+
+    @Override
+    public BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException {
+        final HashBiMap<Integer, CategoryIdAndMetric> accumulator = HashBiMap.create();
+        for (final Map<String, Object> metricInfo : delegate.getMetrics()) {
+            accumulator.put(Integer.valueOf(metricInfo.get("sample_kind_id").toString()),
+                            new CategoryIdAndMetric((Integer) metricInfo.get("event_category_id"), metricInfo.get("sample_kind").toString()));
+        }
+        return accumulator;
+    }
+
+    @Override
+    public synchronized int getOrAddMetric(final Integer sourceId, final Integer eventCategoryId, final String metric) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.begin();
+        delegate.addMetric(eventCategoryId, metric);
+        final Integer metricId = delegate.getMetricId(eventCategoryId, metric);
+        delegate.commit();
+
+        return metricId;
+    }
+
+    @Override
+    public Iterable<Integer> getMetricIdsBySourceId(final Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getMetricIdsBySourceId(sourceId);
+    }
+
+    @Override
+    public Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException {
+        return delegate.getMetricIdsForAllSources();
+    }
+
+    @Override
+    public Long insertTimelineChunk(final TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.begin();
+        delegate.insertTimelineChunk(timelineChunk);
+        final long timelineChunkId = delegate.getLastInsertedId();
+        delegate.commit();
+        return timelineChunkId;
+    }
+
+    @Override
+    public void getSamplesBySourceIdsAndMetricIds(final List<Integer> sourceIdList,
+                                                  @Nullable final List<Integer> metricIdList,
+                                                  final DateTime startTime,
+                                                  final DateTime endTime,
+                                                  final TimelineChunkConsumer chunkConsumer) {
+        dbi.withHandle(new HandleCallback<Void>() {
+            @Override
+            public Void withHandle(final Handle handle) throws Exception {
+                handle.setStatementLocator(new StringTemplate3StatementLocator(TimelineSqlDao.class));
+
+                ResultIterator<TimelineChunk> iterator = null;
+                try {
+                    final Query<Map<String, Object>> query = handle
+                            .createQuery("getSamplesBySourceIdsAndMetricIds")
+                            .bind("startTime", DateTimeUtils.unixSeconds(startTime))
+                            .bind("endTime", DateTimeUtils.unixSeconds(endTime))
+                            .define("sourceIds", JOINER.join(sourceIdList));
+
+                    if (metricIdList != null && !metricIdList.isEmpty()) {
+                        query.define("metricIds", JOINER.join(metricIdList));
+                    }
+
+                    iterator = query
+                            .map(timelineChunkMapper)
+                            .iterator();
+
+                    while (iterator.hasNext()) {
+                        chunkConsumer.processTimelineChunk(iterator.next());
+                    }
+                    return null;
+                } finally {
+                    if (iterator != null) {
+                        try {
+                            iterator.close();
+                        } catch (Exception e) {
+                            log.error("Exception closing TimelineChunkAndTimes iterator for sourceIds {} and metricIds {}", sourceIdList, metricIdList);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    public Integer insertLastStartTimes(final StartTimes startTimes) {
+        return delegate.insertLastStartTimes(startTimes);
+    }
+
+    @Override
+    public StartTimes getLastStartTimes() {
+        return delegate.getLastStartTimes();
+    }
+
+    @Override
+    public void deleteLastStartTimes() {
+        delegate.deleteLastStartTimes();
+    }
+
+    @Override
+    public void test() throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.test();
+    }
+
+    @Override
+    public void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.bulkInsertSources(sources.iterator());
+    }
+
+    @Override
+    public void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException {
+        delegate.bulkInsertEventCategories(categoryNames.iterator());
+    }
+
+    @Override
+    public void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds) {
+        delegate.bulkInsertMetrics(categoryAndKinds.iterator());
+    }
+
+    @Override
+    public void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList) {
+        delegate.bulkInsertTimelineChunks(timelineChunkList.iterator());
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java
new file mode 100644
index 0000000..9040b0d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/FileBackedBuffer.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+import com.fasterxml.jackson.dataformat.smile.SmileGenerator;
+import com.fasterxml.util.membuf.MemBuffersForBytes;
+import com.fasterxml.util.membuf.StreamyBytesMemBuffer;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Backing buffer for a single TimelineSourceEventAccumulator that spools to disk
+ */
+public class FileBackedBuffer {
+
+    private static final Logger log = LoggerFactory.getLogger(FileBackedBuffer.class);
+
+    private static final SmileFactory smileFactory = new SmileFactory();
+    private static final ObjectMapper smileObjectMapper = new ObjectMapper(smileFactory);
+
+    static {
+        // Disable all magic for now as we don't write the Smile header (we share the same smileGenerator
+        // across multiple backend files)
+        smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_NAMES, false);
+        smileFactory.configure(SmileGenerator.Feature.CHECK_SHARED_STRING_VALUES, false);
+    }
+
+    private final String basePath;
+    private final String prefix;
+    private final boolean deleteFilesOnClose;
+    private final AtomicLong samplesforTimestampWritten = new AtomicLong();
+    private final Object recyclingMonitor = new Object();
+
+    private final StreamyBytesMemBuffer inputBuffer;
+    private StreamyBytesPersistentOutputStream out = null;
+    private SmileGenerator smileGenerator;
+
+    public FileBackedBuffer(final String basePath, final String prefix, final int segmentsSize, final int maxNbSegments) throws IOException {
+        this(basePath, prefix, true, segmentsSize, maxNbSegments);
+    }
+
+    public FileBackedBuffer(final String basePath, final String prefix, final boolean deleteFilesOnClose, final int segmentsSize, final int maxNbSegments) throws IOException {
+        this.basePath = basePath;
+        this.prefix = prefix;
+        this.deleteFilesOnClose = deleteFilesOnClose;
+
+        final MemBuffersForBytes bufs = new MemBuffersForBytes(segmentsSize, 1, maxNbSegments);
+        inputBuffer = bufs.createStreamyBuffer(1, maxNbSegments);
+
+        recycle();
+    }
+
+    public boolean append(final SourceSamplesForTimestamp sourceSamplesForTimestamp) {
+        try {
+            synchronized (recyclingMonitor) {
+                smileObjectMapper.writeValue(smileGenerator, sourceSamplesForTimestamp);
+                samplesforTimestampWritten.incrementAndGet();
+                return true;
+            }
+        } catch (IOException e) {
+            log.warn("Unable to backup samples", e);
+            return false;
+        }
+    }
+
+    /**
+     * Discard in-memory and on-disk data
+     */
+    public void discard() {
+        try {
+            recycle();
+        } catch (IOException e) {
+            log.warn("Exception discarding buffer", e);
+        }
+    }
+
+    private void recycle() throws IOException {
+        synchronized (recyclingMonitor) {
+            if (out != null && !out.isEmpty()) {
+                out.close();
+            }
+
+            out = new StreamyBytesPersistentOutputStream(basePath, prefix, inputBuffer, deleteFilesOnClose);
+            smileGenerator = smileFactory.createJsonGenerator(out, JsonEncoding.UTF8);
+            // Drop the Smile header
+            smileGenerator.flush();
+            out.reset();
+
+            samplesforTimestampWritten.set(0);
+        }
+    }
+
+    //@MonitorableManaged(description = "Return the approximate size of bytes on disk for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+    public long getBytesOnDisk() {
+        return out.getBytesOnDisk();
+    }
+
+    //@MonitorableManaged(description = "Return the approximate size of bytes in memory for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+    public long getBytesInMemory() {
+        return out.getBytesInMemory();
+    }
+
+    //@MonitorableManaged(description = "Return the approximate size of bytes available in memory (before spilling over to disk) for samples not yet in the database", monitored = true, monitoringType = {MonitoringType.VALUE})
+    public long getInMemoryAvailableSpace() {
+        return out.getInMemoryAvailableSpace();
+    }
+
+    @VisibleForTesting
+    public long getFilesCreated() {
+        return out.getCreatedFiles().size();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java
new file mode 100644
index 0000000..5896216
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/Replayer.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+import com.fasterxml.jackson.dataformat.smile.SmileParser;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+public class Replayer {
+
+    private static final Logger log = LoggerFactory.getLogger(Replayer.class);
+    private static final SmileFactory smileFactory = new SmileFactory();
+    private static final ObjectMapper smileMapper = new ObjectMapper(smileFactory);
+
+    static {
+        smileFactory.configure(SmileParser.Feature.REQUIRE_HEADER, false);
+        smileFactory.setCodec(smileMapper);
+    }
+
+    @VisibleForTesting
+    public static final Ordering<File> FILE_ORDERING = new Ordering<File>() {
+        @Override
+        public int compare(@Nullable final File left, @Nullable final File right) {
+            if (left == null || right == null) {
+                throw new NullPointerException();
+            }
+
+            // Order by the nano time
+            return left.getAbsolutePath().compareTo(right.getAbsolutePath());
+        }
+    };
+
+    private final String path;
+    private AtomicBoolean shuttingDown = new AtomicBoolean();
+
+    public Replayer(final String path) {
+        this.path = path;
+    }
+
+    // This method is only used by test code
+    public List<SourceSamplesForTimestamp> readAll() {
+        final List<SourceSamplesForTimestamp> samples = new ArrayList<SourceSamplesForTimestamp>();
+
+        readAll(true, null, new Function<SourceSamplesForTimestamp, Void>() {
+            @Override
+            public Void apply(@Nullable final SourceSamplesForTimestamp input) {
+                if (input != null) {
+                    samples.add(input);
+                }
+                return null;
+            }
+        });
+
+        return samples;
+    }
+
+    public void initiateShutdown() {
+        shuttingDown.set(true);
+    }
+
+    public int readAll(final boolean deleteFiles, @Nullable final DateTime minStartTime, final Function<SourceSamplesForTimestamp, Void> fn) {
+        final Collection<File> files = ImmutableList.<File>copyOf(findCandidates());
+        int filesSkipped = 0;
+        for (final File file : FILE_ORDERING.sortedCopy(files)) {
+            try {
+                // Skip files whose last modification date is is earlier than the first start time.
+                if (minStartTime != null && file.lastModified() < minStartTime.getMillis()) {
+                    filesSkipped++;
+                    continue;
+                }
+                read(file, fn);
+                if (shuttingDown.get()) {
+                    break;
+                }
+
+                if (deleteFiles) {
+                    if (!file.delete()) {
+                        log.warn("Unable to delete file: {}", file.getAbsolutePath());
+                    }
+                }
+            } catch (IOException e) {
+                log.warn("Exception replaying file: {}", file.getAbsolutePath(), e);
+            }
+        }
+        return filesSkipped;
+    }
+
+    @VisibleForTesting
+    public void read(final File file, final Function<SourceSamplesForTimestamp, Void> fn) throws IOException {
+        final JsonParser smileParser = smileFactory.createJsonParser(file);
+        if (smileParser.nextToken() != JsonToken.START_ARRAY) {
+            return;
+        }
+
+        while (!shuttingDown.get() && smileParser.nextToken() != JsonToken.END_ARRAY) {
+            final SourceSamplesForTimestamp sourceSamplesForTimestamp = smileParser.readValueAs(SourceSamplesForTimestamp.class);
+            fn.apply(sourceSamplesForTimestamp);
+        }
+
+        smileParser.close();
+    }
+
+
+    public void purgeOldFiles(final DateTime purgeIfOlderDate) {
+        final File[] candidates = findCandidates();
+        for (final File file : candidates) {
+            if (file.lastModified() <= purgeIfOlderDate.getMillis()) {
+                if (!file.delete()) {
+                    log.warn("Unable to delete file: {}", file.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    private File[] findCandidates() {
+        final File root = new File(path);
+        final FilenameFilter filter = new FilenameFilter() {
+            @Override
+            public boolean accept(final File file, final String s) {
+                return s.endsWith("bin");
+            }
+        };
+
+        return root.listFiles(filter);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java
new file mode 100644
index 0000000..1256583
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/StreamyBytesPersistentOutputStream.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.dataformat.smile.SmileConstants;
+import com.fasterxml.util.membuf.StreamyBytesMemBuffer;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Files;
+
+public class StreamyBytesPersistentOutputStream extends OutputStream {
+
+    private static final Logger log = LoggerFactory.getLogger(StreamyBytesPersistentOutputStream.class);
+    private static final int BUF_SIZE = 0x1000; // 4K
+
+    private final String basePath;
+    private final String prefix;
+    private final StreamyBytesMemBuffer inputBuffer;
+    private final boolean deleteFilesOnClose;
+    private final List<String> createdFiles = new ArrayList<String>();
+
+    private long bytesOnDisk = 0L;
+
+    public StreamyBytesPersistentOutputStream(String basePath, final String prefix, final StreamyBytesMemBuffer inputBuffer, final boolean deleteFilesOnClose) {
+        if (!basePath.endsWith("/")) {
+            basePath += "/";
+        }
+        this.basePath = basePath;
+
+        this.prefix = prefix;
+        this.inputBuffer = inputBuffer;
+        this.deleteFilesOnClose = deleteFilesOnClose;
+    }
+
+    @Override
+    public void write(final int b) throws IOException {
+        final byte data = (byte) b;
+        write(new byte[]{data}, 0, 1);
+    }
+
+    @Override
+    public void write(final byte[] data, final int off, final int len) throws IOException {
+        if (!inputBuffer.tryAppend(data, off, len)) {
+            // Buffer full - need to flush
+            flushUnderlyingBufferAndReset();
+
+            if (!inputBuffer.tryAppend(data, off, len)) {
+                log.warn("Unable to append data: 1 byte lost");
+            }
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        // Cleanup volatile data
+        inputBuffer.clear();
+
+        // Cleanup persistent data
+        if (deleteFilesOnClose) {
+            for (final String path : createdFiles) {
+                log.info("Discarding file: {}", path);
+                if (!new File(path).delete()) {
+                    log.warn("Unable to discard file: {}", path);
+                }
+            }
+        }
+    }
+
+    private void flushUnderlyingBufferAndReset() {
+        synchronized (inputBuffer) {
+            if (inputBuffer.available() == 0) {
+                // Somebody beat you to it
+                return;
+            }
+
+            final String pathname = getFileName();
+            createdFiles.add(pathname);
+            log.debug("Flushing in-memory buffer to disk: {}", pathname);
+
+            try {
+                final File out = new File(pathname);
+                flushToFile(out);
+            } catch (IOException e) {
+                log.warn("Error flushing data", e);
+            } finally {
+                reset();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    String getFileName() {
+        return basePath + "arecibo." + prefix + "." + System.nanoTime() + ".bin";
+    }
+
+    private void flushToFile(final File out) throws IOException {
+        final byte[] buf = new byte[BUF_SIZE];
+        FileOutputStream transfer = null;
+
+        int bytesTransferred = 0;
+        try {
+            transfer = Files.newOutputStreamSupplier(out).getOutput();
+
+            while (true) {
+                final int r = inputBuffer.readIfAvailable(buf);
+                if (r == 0) {
+                    break;
+                }
+                transfer.write(buf, 0, r);
+                bytesTransferred += r;
+            }
+        } finally {
+            if (transfer != null) {
+                try {
+                    transfer.write(SmileConstants.TOKEN_LITERAL_END_ARRAY);
+                    bytesTransferred++;
+                    bytesOnDisk += bytesTransferred;
+                } finally {
+                    transfer.flush();
+                }
+            }
+        }
+        log.debug("Saved {} bytes to disk", bytesTransferred);
+    }
+
+    public void reset() {
+        inputBuffer.clear();
+        try {
+            write(SmileConstants.TOKEN_LITERAL_START_ARRAY);
+        } catch (IOException e) {
+            // Not sure how to recover?
+        }
+    }
+
+    public List<String> getCreatedFiles() {
+        return createdFiles;
+    }
+
+    public long getBytesOnDisk() {
+        return bytesOnDisk;
+    }
+
+    public long getBytesInMemory() {
+        return inputBuffer.getTotalPayloadLength();
+    }
+
+    public long getInMemoryAvailableSpace() {
+        return inputBuffer.getMaximumAvailableSpace();
+    }
+
+    public boolean isEmpty() {
+        return inputBuffer.isEmpty();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java
new file mode 100644
index 0000000..6cbdb34
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineDao.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.exceptions.CallbackFailedException;
+import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.consumer.TimelineChunkConsumer;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+
+import com.google.common.collect.BiMap;
+
+public interface TimelineDao {
+
+    // Sources table
+
+    Integer getSourceId(String source) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    String getSource(Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    BiMap<Integer, String> getSources() throws UnableToObtainConnectionException, CallbackFailedException;
+
+    int getOrAddSource(String source) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    // Event categories table
+
+    Integer getEventCategoryId(String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    String getEventCategory(Integer eventCategoryId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    BiMap<Integer, String> getEventCategories() throws UnableToObtainConnectionException, CallbackFailedException;
+
+    int getOrAddEventCategory(String eventCategory) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    // Sample kinds table
+
+    Integer getMetricId(int eventCategory, String metric) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    CategoryIdAndMetric getCategoryIdAndMetric(Integer metricId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    BiMap<Integer, CategoryIdAndMetric> getMetrics() throws UnableToObtainConnectionException, CallbackFailedException;
+
+    int getOrAddMetric(Integer sourceId, Integer eventCategoryId, String metric) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    Iterable<Integer> getMetricIdsBySourceId(Integer sourceId) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    Iterable<SourceIdAndMetricId> getMetricIdsForAllSources() throws UnableToObtainConnectionException, CallbackFailedException;
+
+    // Timelines tables
+
+    Long insertTimelineChunk(TimelineChunk timelineChunk) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    void getSamplesBySourceIdsAndMetricIds(List<Integer> sourceIds,
+                                           @Nullable List<Integer> metricIds,
+                                           DateTime startTime,
+                                           DateTime endTime,
+                                           TimelineChunkConsumer chunkConsumer) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    Integer insertLastStartTimes(StartTimes startTimes);
+
+    StartTimes getLastStartTimes();
+
+    void deleteLastStartTimes();
+
+    void bulkInsertSources(final List<String> sources) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    void bulkInsertEventCategories(final List<String> categoryNames) throws UnableToObtainConnectionException, CallbackFailedException;
+
+    void bulkInsertMetrics(final List<CategoryIdAndMetric> categoryAndKinds);
+
+    void bulkInsertTimelineChunks(final List<TimelineChunk> timelineChunkList);
+
+    void test() throws UnableToObtainConnectionException, CallbackFailedException;
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java
new file mode 100644
index 0000000..ce6c5bd
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.skife.jdbi.v2.DefaultMapper;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlBatch;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.BatchChunkSize;
+import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
+import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
+import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
+import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
+
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetric;
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricBinder;
+import com.ning.billing.usage.timeline.categories.CategoryIdAndMetricMapper;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunkBinder;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.shutdown.StartTimesBinder;
+import com.ning.billing.usage.timeline.shutdown.StartTimesMapper;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricId;
+import com.ning.billing.usage.timeline.sources.SourceIdAndMetricIdMapper;
+
+@ExternalizedSqlViaStringTemplate3()
+@RegisterMapper({CategoryIdAndMetricMapper.class, StartTimesMapper.class, SourceIdAndMetricIdMapper.class})
+public interface TimelineSqlDao extends Transactional<TimelineSqlDao>, Transmogrifier {
+
+    @SqlQuery
+    Integer getSourceId(@Bind("sourceName") final String source);
+
+    @SqlQuery
+    String getSource(@Bind("sourceId") final Integer sourceId);
+
+    @SqlQuery
+    @Mapper(DefaultMapper.class)
+    List<Map<String, Object>> getSources();
+
+    @SqlUpdate
+    void addSource(@Bind("sourceName") final String source);
+
+    @SqlBatch
+    @BatchChunkSize(1000)
+    void bulkInsertSources(@Bind("sourceName") Iterator<String> sourcesIterator);
+
+    @SqlQuery
+    Integer getEventCategoryId(@Bind("eventCategory") final String eventCategory);
+
+    @SqlQuery
+    String getEventCategory(@Bind("eventCategoryId") final Integer eventCategoryId);
+
+    @SqlUpdate
+    void addEventCategory(@Bind("eventCategory") final String eventCategory);
+
+    @SqlBatch
+    @BatchChunkSize(1000)
+    void bulkInsertEventCategories(@Bind("eventCategory") Iterator<String> cateogoryNames);
+
+    @SqlQuery
+    Iterable<Integer> getMetricIdsBySourceId(@Bind("sourceId") final Integer sourceId);
+
+    @SqlQuery
+    Iterable<SourceIdAndMetricId> getMetricIdsForAllSources();
+
+    @SqlQuery
+    Integer getMetricId(@Bind("eventCategoryId") final int eventCategoryId, @Bind("metric") final String metric);
+
+    @SqlQuery
+    CategoryIdAndMetric getEventCategoryIdAndMetric(@Bind("metricId") final Integer metricId);
+
+    @SqlUpdate
+    void addMetric(@Bind("eventCategoryId") final int eventCategoryId, @Bind("metric") final String metric);
+
+    @SqlBatch
+    @BatchChunkSize(1000)
+    void bulkInsertMetrics(@CategoryIdAndMetricBinder Iterator<CategoryIdAndMetric> categoriesAndMetrics);
+
+    @SqlQuery
+    @Mapper(DefaultMapper.class)
+    List<Map<String, Object>> getEventCategories();
+
+    @SqlQuery
+    @Mapper(DefaultMapper.class)
+    List<Map<String, Object>> getMetrics();
+
+    @SqlQuery
+    int getLastInsertedId();
+
+    @SqlQuery
+    long getHighestTimelineChunkId();
+
+    @SqlUpdate
+    void insertTimelineChunk(@TimelineChunkBinder final TimelineChunk timelineChunk);
+
+    @SqlBatch
+    @BatchChunkSize(1000)
+    void bulkInsertTimelineChunks(@TimelineChunkBinder Iterator<TimelineChunk> chunkIterator);
+
+    @SqlUpdate
+    Integer insertLastStartTimes(@StartTimesBinder final StartTimes startTimes);
+
+    @SqlQuery
+    StartTimes getLastStartTimes();
+
+    @SqlUpdate
+    void deleteLastStartTimes();
+
+    @SqlUpdate
+    void test();
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java
new file mode 100644
index 0000000..90f9e77
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/HalfFloat.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+public class HalfFloat {
+
+    private final short halfFloat;
+
+    public HalfFloat(final float input) {
+        halfFloat = (short)fromFloat(input);
+    }
+
+    public float getFloat() {
+        return toFloat((int)halfFloat);
+    }
+
+    // These two static methods were pinched from http://stackoverflow.com/questions/6162651/half-precision-floating-point-in-java/6162687#6162687
+    // The last comment on that page is the author saying "I hereby commit these to the public domain"
+
+    // Ignores the higher 16 bits
+    public static float toFloat(final int hbits) {
+        int mant = hbits & 0x03ff;                                   // 10 bits mantissa
+        int exp =  hbits & 0x7c00;                                   // 5 bits exponent
+        if (exp == 0x7c00) {                                         // NaN/Inf
+            exp = 0x3fc00;                                           // -> NaN/Inf
+        }
+        else if (exp != 0) {                                         // normalized value
+            exp += 0x1c000;                                          // exp - 15 + 127
+            if(mant == 0 && exp > 0x1c400) {                         // smooth transition
+                return Float.intBitsToFloat((hbits & 0x8000) << 16 | exp << 13 | 0x3ff);
+            }
+        }
+        else if (mant != 0) {                                        // && exp==0 -> subnormal
+            exp = 0x1c400;                                           // make it normal
+            do {
+                mant <<= 1;                                          // mantissa * 2
+                exp -= 0x400;                                        // decrease exp by 1
+            } while ((mant & 0x400) == 0);                           // while not normal
+            mant &= 0x3ff;                                           // discard subnormal bit
+        }                                                            // else +/-0 -> +/-0
+        return Float.intBitsToFloat(                                 // combine all parts
+            (hbits & 0x8000) << 16                                   // sign  << (31 - 15)
+            | (exp | mant) << 13);                                   // value << (23 - 10)
+    }
+
+    // returns all higher 16 bits as 0 for all results
+    public static int fromFloat(final float fval) {
+        final int fbits = Float.floatToIntBits(fval);
+        final int sign = fbits >>> 16 & 0x8000;                      // sign only
+        int val = (fbits & 0x7fffffff) + 0x1000;                     // rounded value
+
+        if (val >= 0x47800000) {                                     // might be or become NaN/Inf
+            if ((fbits & 0x7fffffff) >= 0x47800000) {                // is or must become NaN/Inf
+
+                if (val < 0x7f800000) {                              // was value but too large
+                    return sign | 0x7c00;                            // make it +/-Inf
+                }
+                return sign | 0x7c00 |                               // remains +/-Inf or NaN
+                    (fbits & 0x007fffff) >>> 13;                     // keep NaN (and Inf) bits
+            }
+            return sign | 0x7bff;                                    // unrounded not quite Inf
+        }
+        if(val >= 0x38800000)                                        // remains normalized value
+            return sign | val - 0x38000000 >>> 13;                   // exp - 127 + 15
+        if(val < 0x33000000)                                         // too small for subnormal
+            return sign;                                             // becomes +/-0
+        val = (fbits & 0x7fffffff) >>> 23;                           // tmp exp for subnormal calc
+        return sign | ((fbits & 0x7fffff | 0x800000)                 // add subnormal bit
+             + (0x800000 >>> val - 102) >>> 126 - val);              // round depending on cut off; div by 2^(1-(exp-127+15)) and >> 13 | exp=0
+    }
+
+    private static String describe(final int halfFloat) {
+        final int sign = (halfFloat >> 15) & 1;
+        final int exponent = (halfFloat >> 10) & 0x1f;
+        final double fraction = (double)(0x400 + (halfFloat & 0x3ff)) / (1.0 * 0x400);
+        final double product = fraction * Math.pow(2.0, exponent - 15) * (sign == 0 ? 1.0 : -1.0);
+        return String.format("HalfFloat %f, representation %x, sign %d, exponent 0x%x == 2**%d, fraction %f, product %f",
+                toFloat(halfFloat),
+                halfFloat,
+                sign,
+                exponent,
+                exponent - 15,
+                fraction,
+                product);
+    }
+
+    public static void main(String[] args) {
+        System.out.printf("%f double-converted = %f\n", 200.0, toFloat(fromFloat((float)200.0)));
+        System.out.printf("%s\n", describe(fromFloat(200.0f)));
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java
new file mode 100644
index 0000000..efa99fa
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/NullSample.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+public class NullSample extends ScalarSample<Void> {
+
+    public NullSample() {
+        super(SampleOpcode.NULL, null);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java
new file mode 100644
index 0000000..990ef64
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/RepeatSample.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+/**
+ * A repeated value
+ *
+ * @param <T> A value consistent with the opcode
+ */
+public class RepeatSample<T> extends SampleBase {
+
+    public static final int MAX_BYTE_REPEAT_COUNT = 0xFF; // The maximum byte value
+    public static final int MAX_SHORT_REPEAT_COUNT = 0xFFFF; // The maximum short value
+
+    private final ScalarSample<T> sampleRepeated;
+
+    private int repeatCount;
+
+    public RepeatSample(final int repeatCount, final ScalarSample<T> sampleRepeated) {
+        super(SampleOpcode.REPEAT_BYTE);
+        this.repeatCount = repeatCount;
+        this.sampleRepeated = sampleRepeated;
+    }
+
+    public int getRepeatCount() {
+        return repeatCount;
+    }
+
+    public void incrementRepeatCount() {
+        repeatCount++;
+    }
+
+    public void incrementRepeatCount(final int addend) {
+        repeatCount += addend;
+    }
+
+    public ScalarSample<T> getSampleRepeated() {
+        return sampleRepeated;
+    }
+
+    @Override
+    public SampleOpcode getOpcode() {
+        return repeatCount > MAX_BYTE_REPEAT_COUNT ? SampleOpcode.REPEAT_SHORT : SampleOpcode.REPEAT_BYTE;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("RepeatSample");
+        sb.append("{sampleRepeated=").append(sampleRepeated);
+        sb.append(", repeatCount=").append(repeatCount);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final RepeatSample that = (RepeatSample) o;
+
+        if (repeatCount != that.repeatCount) {
+            return false;
+        }
+        if (sampleRepeated != null ? !sampleRepeated.equals(that.sampleRepeated) : that.sampleRepeated != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = sampleRepeated != null ? sampleRepeated.hashCode() : 0;
+        result = 31 * result + repeatCount;
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java
new file mode 100644
index 0000000..25072e5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleBase.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+public abstract class SampleBase {
+
+    protected final SampleOpcode opcode;
+
+    public SampleBase(final SampleOpcode opcode) {
+        this.opcode = opcode;
+    }
+
+    public SampleOpcode getOpcode() {
+        return opcode;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java
new file mode 100644
index 0000000..a6823dd
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/SampleOpcode.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Opcode for samples encoding
+ */
+public enum SampleOpcode {
+    BYTE(1, 1),
+    SHORT(2, 2),
+    INT(3, 4),
+    LONG(4, 8),
+    FLOAT(5, 4),
+    DOUBLE(6, 8),
+    STRING(7, 0),
+    NULL(8, 0, true),
+    FLOAT_FOR_DOUBLE(10, 4, DOUBLE),
+    HALF_FLOAT_FOR_DOUBLE(11, 2, DOUBLE),
+    BYTE_FOR_DOUBLE(12, 1, DOUBLE),
+    SHORT_FOR_DOUBLE(13, 2, DOUBLE),
+    BIGINT(14, 0),
+    DOUBLE_ZERO(15, 0, true),
+    INT_ZERO(16, 0, true),
+    REPEAT_BYTE(0xff, 1, true),   // A repeat operation in which the repeat count fits in an unsigned byte
+    REPEAT_SHORT(0xfe, 2, true);  // A repeat operation in which the repeat count fits in an unsigned short
+
+    private static final Logger log = LoggerFactory.getLogger(SampleOpcode.class);
+
+    private int opcodeIndex;
+    private final int byteSize;
+    private final boolean repeater;
+    private final boolean noArgs;
+    private final SampleOpcode replacement;
+
+    private SampleOpcode(int opcodeIndex, int byteSize) {
+        this(opcodeIndex, byteSize, false);
+    }
+
+    private SampleOpcode(int opcodeIndex, int byteSize, boolean noArgs) {
+        this(opcodeIndex, byteSize, noArgs, false);
+    }
+
+    private SampleOpcode(int opcodeIndex, int byteSize, boolean noArgs, boolean repeater) {
+        this.opcodeIndex = opcodeIndex;
+        this.byteSize = byteSize;
+        this.noArgs = noArgs;
+        this.repeater = repeater;
+        this.replacement = this;
+    }
+
+    private SampleOpcode(int opcodeIndex, int byteSize, SampleOpcode replacement) {
+        this(opcodeIndex, byteSize, false, false, replacement);
+    }
+
+    private SampleOpcode(final int opcodeIndex, final int byteSize, final boolean noArgs, final boolean repeater, final SampleOpcode replacement) {
+        this.opcodeIndex = opcodeIndex;
+        this.byteSize = byteSize;
+        this.noArgs = noArgs;
+        this.repeater = repeater;
+        this.replacement = replacement;
+    }
+
+    public static SampleOpcode getOpcodeFromIndex(final int index) {
+        for (SampleOpcode opcode : values()) {
+            if (opcode.getOpcodeIndex() == index) {
+                return opcode;
+            }
+        }
+
+        final String s = String.format("In SampleOpcode.getOpcodefromIndex(), could not find opcode for index %d", index);
+        log.error(s);
+        throw new IllegalArgumentException(s);
+    }
+
+    public int getOpcodeIndex() {
+        return opcodeIndex;
+    }
+
+    public int getByteSize() {
+        return byteSize;
+    }
+
+    public boolean getNoArgs() {
+        return noArgs;
+    }
+
+    public boolean getRepeater() {
+        return repeater;
+    }
+
+    public SampleOpcode getReplacement() {
+        return replacement;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SampleOpcode");
+        sb.append("{opcodeIndex=").append(opcodeIndex);
+        sb.append(", byteSize=").append(byteSize);
+        sb.append(", repeater=").append(repeater);
+        sb.append(", noArgs=").append(noArgs);
+        sb.append(", replacement=").append(replacement.name());
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java b/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java
new file mode 100644
index 0000000..a4f3173
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/samples/ScalarSample.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigInteger;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Shorts;
+
+/**
+ * A sample value associated with its opcode
+ *
+ * @param <T> A value consistent with the opcode
+ */
+public class ScalarSample<T> extends SampleBase {
+
+    private static final String KEY_OPCODE = "O";
+    private static final String KEY_SAMPLE_CLASS = "K";
+    private static final String KEY_SAMPLE_VALUE = "V";
+
+    private final T sampleValue;
+
+    public static ScalarSample fromObject(final Object sampleValue) {
+        if (sampleValue == null) {
+            return new ScalarSample<Void>(SampleOpcode.NULL, null);
+        } else if (sampleValue instanceof Byte) {
+            return new ScalarSample<Byte>(SampleOpcode.BYTE, (Byte) sampleValue);
+        } else if (sampleValue instanceof Short) {
+            return new ScalarSample<Short>(SampleOpcode.SHORT, (Short) sampleValue);
+        } else if (sampleValue instanceof Integer) {
+            try {
+                // Can it fit in a short?
+                final short optimizedShort = Shorts.checkedCast(Long.valueOf(sampleValue.toString()));
+                return new ScalarSample<Short>(SampleOpcode.SHORT, optimizedShort);
+            } catch (IllegalArgumentException e) {
+                return new ScalarSample<Integer>(SampleOpcode.INT, (Integer) sampleValue);
+            }
+        } else if (sampleValue instanceof Long) {
+            try {
+                // Can it fit in a short?
+                final short optimizedShort = Shorts.checkedCast(Long.valueOf(sampleValue.toString()));
+                return new ScalarSample<Short>(SampleOpcode.SHORT, optimizedShort);
+            } catch (IllegalArgumentException e) {
+                try {
+                    // Can it fit in an int?
+                    final int optimizedLong = Ints.checkedCast(Long.valueOf(sampleValue.toString()));
+                    return new ScalarSample<Integer>(SampleOpcode.INT, optimizedLong);
+                } catch (IllegalArgumentException ohWell) {
+                    return new ScalarSample<Long>(SampleOpcode.LONG, (Long) sampleValue);
+                }
+            }
+        } else if (sampleValue instanceof Float) {
+            return new ScalarSample<Float>(SampleOpcode.FLOAT, (Float) sampleValue);
+        } else if (sampleValue instanceof Double) {
+            return new ScalarSample<Double>(SampleOpcode.DOUBLE, (Double) sampleValue);
+        } else {
+            return new ScalarSample<String>(SampleOpcode.STRING, sampleValue.toString());
+        }
+    }
+
+    public ScalarSample(final SampleOpcode opcode, final T sampleValue) {
+        super(opcode);
+        this.sampleValue = sampleValue;
+    }
+
+    public ScalarSample(final String opcode, final T sampleValue) {
+        this(SampleOpcode.valueOf(opcode), sampleValue);
+    }
+
+    public double getDoubleValue() {
+        final Object sampleValue = getSampleValue();
+        return getDoubleValue(getOpcode(), sampleValue);
+    }
+
+    public static double getDoubleValue(final SampleOpcode opcode, final Object sampleValue) {
+        switch (opcode) {
+            case NULL:
+            case DOUBLE_ZERO:
+            case INT_ZERO:
+                return 0.0;
+            case BYTE:
+            case BYTE_FOR_DOUBLE:
+                return (double) ((Byte) sampleValue);
+            case SHORT:
+            case SHORT_FOR_DOUBLE:
+                return (double) ((Short) sampleValue);
+            case INT:
+                return (double) ((Integer) sampleValue);
+            case LONG:
+                return (double) ((Long) sampleValue);
+            case FLOAT:
+            case FLOAT_FOR_DOUBLE:
+                return (double) ((Float) sampleValue);
+            case HALF_FLOAT_FOR_DOUBLE:
+                return (double) HalfFloat.toFloat((Short) sampleValue);
+            case DOUBLE:
+                return (Double) sampleValue;
+            case BIGINT:
+                return ((BigInteger) sampleValue).doubleValue();
+            default:
+                throw new IllegalArgumentException(String.format("In getDoubleValue(), sample opcode is %s, sample value is %s",
+                                                                 opcode.name(), String.valueOf(sampleValue)));
+        }
+    }
+
+    @JsonCreator
+    public ScalarSample(@JsonProperty(KEY_OPCODE) final byte opcodeIdx,
+                        @JsonProperty(KEY_SAMPLE_CLASS) final Class klass,
+                        @JsonProperty(KEY_SAMPLE_VALUE) final T sampleValue) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
+        super(SampleOpcode.getOpcodeFromIndex(opcodeIdx));
+        // Numerical classes have a String constructor
+        this.sampleValue = (T) klass.getConstructor(String.class).newInstance(sampleValue.toString());
+    }
+
+    @JsonValue
+    public Map<String, Object> toMap() {
+        // Work around type erasure by storing explicitly the sample class. This avoid deserializing shorts as integers
+        // at replay time for instance
+        return ImmutableMap.of(KEY_OPCODE, opcode.getOpcodeIndex(), KEY_SAMPLE_CLASS, sampleValue.getClass(), KEY_SAMPLE_VALUE, sampleValue);
+    }
+
+    public T getSampleValue() {
+        return sampleValue;
+    }
+
+    @Override
+    public String toString() {
+        return sampleValue.toString();
+    }
+
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null || !(other instanceof SampleBase)) {
+            return false;
+        }
+        final ScalarSample otherSample = (ScalarSample) other;
+        final Object otherValue = otherSample.getSampleValue();
+        if (getOpcode() != otherSample.getOpcode()) {
+            return false;
+        } else if (!opcode.getNoArgs() && !(sameSampleValues(sampleValue, otherValue))) {
+            return false;
+        }
+        return true;
+    }
+
+    public static boolean sameSampleValues(final Object o1, final Object o2) {
+        if (o1 == o2) {
+            return true;
+        } else if (o1.getClass() == o2.getClass()) {
+            return o1.equals(o2);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return sampleValue != null ? sampleValue.hashCode() : 0;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java
new file mode 100644
index 0000000..9ce0ebe
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/ShutdownSaveMode.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.shutdown;
+
+public enum ShutdownSaveMode {
+    SAVE_ALL_TIMELINES,    // Save all timelines in the db
+    SAVE_START_TIMES;      // Save just the start times for each timeline, and use the replay facility to reconstruct the accumulators on startup
+
+    public static ShutdownSaveMode fromString(final String mode) {
+        for (final ShutdownSaveMode s : ShutdownSaveMode.values()) {
+            if (s.name().equalsIgnoreCase(mode)) {
+                return s;
+            }
+        }
+        throw new IllegalArgumentException(String.format("The argument %s was supposed to be a ShutdownSaveMode, but was not", mode));
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java
new file mode 100644
index 0000000..7e1bc9d
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimes.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.shutdown;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+/**
+ * This class is used solely as a Json mapping class when saving timelines in a database
+ * blob on shutdown, and restoring them on startup.
+ * <p/>
+ * The Map<Integer, Map<Integer, DateTime>> maps from sourceId to eventCategoryId to startTime.
+ */
+public class StartTimes {
+
+    private final DateTime timeInserted;
+    private final Map<Integer, Map<Integer, DateTime>> startTimesMap;
+    private DateTime minStartTime;
+
+    public StartTimes(final DateTime timeInserted, final Map<Integer, Map<Integer, DateTime>> startTimesMap) {
+        this.timeInserted = timeInserted;
+        this.startTimesMap = startTimesMap;
+        DateTime minDateTime = new DateTime(Long.MAX_VALUE);
+        for (final Map<Integer, DateTime> categoryMap : startTimesMap.values()) {
+            for (final DateTime startTime : categoryMap.values()) {
+                if (minDateTime.isAfter(startTime)) {
+                    minDateTime = startTime;
+                }
+            }
+        }
+        this.minStartTime = minDateTime;
+    }
+
+    public StartTimes() {
+        this.timeInserted = new DateTime();
+        minStartTime = new DateTime(Long.MAX_VALUE);
+        this.startTimesMap = new HashMap<Integer, Map<Integer, DateTime>>();
+    }
+
+    public void addTime(final int sourceId, final int categoryId, final DateTime dateTime) {
+        Map<Integer, DateTime> sourceTimes = startTimesMap.get(sourceId);
+        if (sourceTimes == null) {
+            sourceTimes = new HashMap<Integer, DateTime>();
+            startTimesMap.put(sourceId, sourceTimes);
+        }
+        sourceTimes.put(categoryId, dateTime);
+        if (dateTime.isBefore(minStartTime)) {
+            minStartTime = dateTime;
+        }
+    }
+
+    public DateTime getStartTimeForSourceIdAndCategoryId(final int sourceId, final int categoryId) {
+        final Map<Integer, DateTime> sourceTimes = startTimesMap.get(sourceId);
+        if (sourceTimes != null) {
+            return sourceTimes.get(categoryId);
+        } else {
+            return null;
+        }
+    }
+
+    public Map<Integer, Map<Integer, DateTime>> getStartTimesMap() {
+        return startTimesMap;
+    }
+
+    public DateTime getTimeInserted() {
+        return timeInserted;
+    }
+
+    public DateTime getMinStartTime() {
+        return minStartTime;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java
new file mode 100644
index 0000000..aa174a3
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesBinder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.shutdown;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.shutdown.StartTimesBinder.StartTimesBinderFactory;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@BindingAnnotation(StartTimesBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface StartTimesBinder {
+
+    public static class StartTimesBinderFactory implements BinderFactory {
+
+        private static final Logger log = LoggerFactory.getLogger(StartTimesBinderFactory.class);
+        private static final ObjectMapper mapper = new ObjectMapper();
+
+        public Binder build(final Annotation annotation) {
+            return new Binder<StartTimesBinder, StartTimes>() {
+                public void bind(final SQLStatement query, final StartTimesBinder binder, final StartTimes startTimes) {
+                    try {
+                        final String s = mapper.writeValueAsString(startTimes.getStartTimesMap());
+                        query.bind("startTimes", s)
+                             .bind("timeInserted", DateTimeUtils.unixSeconds(startTimes.getTimeInserted()));
+                    } catch (IOException e) {
+                        log.error("Exception while binding StartTimes", e);
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java
new file mode 100644
index 0000000..59d4b25
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/shutdown/StartTimesMapper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.shutdown;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class StartTimesMapper implements ResultSetMapper<StartTimes> {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    @Override
+    public StartTimes map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        try {
+            return new StartTimes(DateTimeUtils.dateTimeFromUnixSeconds(r.getInt("time_inserted")),
+                                  (Map<Integer, Map<Integer, DateTime>>) mapper.readValue(r.getBlob("start_times").getBinaryStream(), new TypeReference<Map<Integer, Map<Integer, DateTime>>>() {
+                                  }));
+        } catch (IOException e) {
+            throw new IllegalStateException(String.format("Could not decode the StartTimes map"), e);
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java
new file mode 100644
index 0000000..424f8be
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceAndId.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.sources;
+
+/**
+ * This class represents one row in the sources table
+ */
+public class SourceAndId {
+
+    private final String source;
+    private final int sourceId;
+
+    public SourceAndId(final String source, final int sourceId) {
+        this.source = source;
+        this.sourceId = sourceId;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SourceAndId");
+        sb.append("{source='").append(source).append('\'');
+        sb.append(", sourceId=").append(sourceId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final SourceAndId that = (SourceAndId) o;
+
+        if (sourceId != that.sourceId) {
+            return false;
+        }
+        if (source != null ? !source.equals(that.source) : that.source != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = source != null ? source.hashCode() : 0;
+        result = 31 * result + sourceId;
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java
new file mode 100644
index 0000000..087eb45
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricId.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.sources;
+
+public class SourceIdAndMetricId {
+
+    private final int sourceId;
+    private final int metricId;
+
+    public SourceIdAndMetricId(final int sourceId, final int metricId) {
+        this.sourceId = sourceId;
+        this.metricId = metricId;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    public int getMetricId() {
+        return metricId;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SourceIdAndMetricId");
+        sb.append("{sourceId=").append(sourceId);
+        sb.append(", metricId=").append(metricId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final SourceIdAndMetricId that = (SourceIdAndMetricId) o;
+
+        if (metricId != that.metricId) {
+            return false;
+        }
+        if (sourceId != that.sourceId) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = sourceId;
+        result = 31 * result + metricId;
+        return result;
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java
new file mode 100644
index 0000000..2959bd5
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceIdAndMetricIdMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.sources;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class SourceIdAndMetricIdMapper implements ResultSetMapper<SourceIdAndMetricId> {
+
+    @Override
+    public SourceIdAndMetricId map(final int index, final ResultSet rs, final StatementContext ctx) throws SQLException {
+        return new SourceIdAndMetricId(rs.getInt("source_id"), rs.getInt("sample_kind_id"));
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java
new file mode 100644
index 0000000..1e7f218
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceMapper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.sources;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class SourceMapper implements ResultSetMapper<SourceAndId> {
+
+    @Override
+    public SourceAndId map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        return new SourceAndId(r.getString("source"), r.getInt("id"));
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java
new file mode 100644
index 0000000..62dee6b
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/sources/SourceSamplesForTimestamp.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.sources;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Instances of this class represent samples sent from one source and one
+ * category, e.g., JVM, representing one point in time.
+ */
+@SuppressWarnings("unchecked")
+public class SourceSamplesForTimestamp {
+
+    private static final String KEY_SOURCE = "H";
+    private static final String KEY_CATEGORY = "V";
+    private static final String KEY_TIMESTAMP = "T";
+    private static final String KEY_SAMPLES = "S";
+
+    private final Integer sourceId;
+    private final String category;
+    private final DateTime timestamp;
+    // A map from sample id to sample value for that timestamp
+    private final Map<Integer, ScalarSample> samples;
+
+    public SourceSamplesForTimestamp(final int sourceId, final String category, final DateTime timestamp) {
+        this(sourceId, category, timestamp, new HashMap<Integer, ScalarSample>());
+    }
+
+    @JsonCreator
+    public SourceSamplesForTimestamp(@JsonProperty(KEY_SOURCE) final Integer sourceId, @JsonProperty(KEY_CATEGORY) final String category,
+                                     @JsonProperty(KEY_TIMESTAMP) final DateTime timestamp, @JsonProperty(KEY_SAMPLES) final Map<Integer, ScalarSample> samples) {
+        this.sourceId = sourceId;
+        this.category = category;
+        this.timestamp = timestamp;
+        this.samples = samples;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    public String getCategory() {
+        return category;
+    }
+
+    public DateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public Map<Integer, ScalarSample> getSamples() {
+        return samples;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("SourceSamplesForTimestamp");
+        sb.append("{category='").append(category).append('\'');
+        sb.append(", sourceId=").append(sourceId);
+        sb.append(", timestamp=").append(timestamp);
+        sb.append(", samples=").append(samples);
+        sb.append('}');
+
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final SourceSamplesForTimestamp that = (SourceSamplesForTimestamp) o;
+
+        if (category != null ? !category.equals(that.category) : that.category != null) {
+            return false;
+        }
+        if (samples != null ? !samples.equals(that.samples) : that.samples != null) {
+            return false;
+        }
+        if (sourceId != null ? !sourceId.equals(that.sourceId) : that.sourceId != null) {
+            return false;
+        }
+        if (timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = sourceId != null ? sourceId.hashCode() : 0;
+        result = 31 * result + (category != null ? category.hashCode() : 0);
+        result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0);
+        result = 31 * result + (samples != null ? samples.hashCode() : 0);
+        return result;
+    }
+
+    @JsonValue
+    public Map<String, Object> toMap() {
+        return ImmutableMap.of(KEY_SOURCE, sourceId, KEY_CATEGORY, category, KEY_TIMESTAMP, timestamp, KEY_SAMPLES, samples);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java
new file mode 100644
index 0000000..f36c1df
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineEventHandler.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.annotation.Nullable;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.config.UsageConfig;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.codec.TimelineChunkAccumulator;
+import com.ning.billing.usage.timeline.persistent.FileBackedBuffer;
+import com.ning.billing.usage.timeline.persistent.Replayer;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.shutdown.ShutdownSaveMode;
+import com.ning.billing.usage.timeline.shutdown.StartTimes;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+public class TimelineEventHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(TimelineEventHandler.class);
+    private final ScheduledExecutorService purgeThread = Executors.newSingleThreadScheduledExecutor();
+    private static final Comparator<TimelineChunk> CHUNK_COMPARATOR = new Comparator<TimelineChunk>() {
+
+        @Override
+        public int compare(final TimelineChunk o1, final TimelineChunk o2) {
+            final int hostDiff = o1.getSourceId() - o1.getSourceId();
+            if (hostDiff < 0) {
+                return -1;
+            } else if (hostDiff > 0) {
+                return 1;
+            } else {
+                final int metricIdDiff = o1.getMetricId() - o2.getMetricId();
+                if (metricIdDiff < 0) {
+                    return -1;
+                } else if (metricIdDiff > 0) {
+                    return 1;
+                } else {
+                    final long startTimeDiff = o1.getStartTime().getMillis() - o2.getStartTime().getMillis();
+                    if (startTimeDiff < 0) {
+                        return -1;
+                    } else if (startTimeDiff > 0) {
+                        return 1;
+                    } else {
+                        return 0;
+                    }
+                }
+            }
+        }
+    };
+
+    // A TimelineSourceEventAccumulator records attributes for a specific host and event type.
+    // This cache maps sourceId -> categoryId -> accumulator
+    //
+    // TODO: There are still timing windows in the use of accumulators.  Enumerate them and
+    // either fix them or prove they are benign
+    private final Map<Integer, SourceAccumulatorsAndUpdateDate> accumulators = new ConcurrentHashMap<Integer, SourceAccumulatorsAndUpdateDate>();
+
+    private final UsageConfig config;
+    private final TimelineDao timelineDAO;
+    private final TimelineCoder timelineCoder;
+    private final SampleCoder sampleCoder;
+    private final BackgroundDBChunkWriter backgroundWriter;
+    private final FileBackedBuffer backingBuffer;
+
+    private final ShutdownSaveMode shutdownSaveMode;
+    private final AtomicBoolean shuttingDown = new AtomicBoolean();
+    private final AtomicBoolean replaying = new AtomicBoolean();
+
+    private final AtomicLong eventsDiscarded = new AtomicLong(0L);
+    private final AtomicLong eventsReceivedAfterShuttingDown = new AtomicLong();
+    private final AtomicLong handledEventCount = new AtomicLong();
+    private final AtomicLong addedSourceEventAccumulatorMapCount = new AtomicLong();
+    private final AtomicLong addedSourceEventAccumulatorCount = new AtomicLong();
+    private final AtomicLong getInMemoryChunksCallCount = new AtomicLong();
+    private final AtomicLong accumulatorDeepCopyCount = new AtomicLong();
+    private final AtomicLong inMemoryChunksReturnedCount = new AtomicLong();
+    private final AtomicLong replayCount = new AtomicLong();
+    private final AtomicLong replaySamplesFoundCount = new AtomicLong();
+    private final AtomicLong replaySamplesOutsideTimeRangeCount = new AtomicLong();
+    private final AtomicLong replaySamplesProcessedCount = new AtomicLong();
+    private final AtomicLong forceCommitCallCount = new AtomicLong();
+    private final AtomicLong purgedAccumsBecauseSourceNotUpdated = new AtomicLong();
+    private final AtomicLong purgedAccumsBecauseCategoryNotUpdated = new AtomicLong();
+
+    @Inject
+    public TimelineEventHandler(final UsageConfig config, final TimelineDao timelineDAO, final TimelineCoder timelineCoder, final SampleCoder sampleCoder, final BackgroundDBChunkWriter backgroundWriter, final FileBackedBuffer fileBackedBuffer) {
+        this.config = config;
+        this.timelineDAO = timelineDAO;
+        this.timelineCoder = timelineCoder;
+        this.sampleCoder = sampleCoder;
+        this.backgroundWriter = backgroundWriter;
+        this.backingBuffer = fileBackedBuffer;
+        this.shutdownSaveMode = ShutdownSaveMode.fromString(config.getShutdownSaveMode());
+    }
+
+    private void saveAccumulators() {
+        for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+            final int sourceId = entry.getKey();
+            final Map<Integer, TimelineSourceEventAccumulator> hostAccumulators = entry.getValue().getCategoryAccumulators();
+            for (final Map.Entry<Integer, TimelineSourceEventAccumulator> accumulatorEntry : hostAccumulators.entrySet()) {
+                final int categoryId = accumulatorEntry.getKey();
+                final TimelineSourceEventAccumulator accumulator = accumulatorEntry.getValue();
+                log.debug("Saving Timeline for sourceId [{}] and categoryId [{}]", sourceId, categoryId);
+                accumulator.extractAndQueueTimelineChunks();
+            }
+        }
+    }
+
+    private void saveStartTimes(final StartTimes startTimes) {
+        for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+            final int sourceId = entry.getKey();
+            final Map<Integer, TimelineSourceEventAccumulator> hostAccumulators = entry.getValue().getCategoryAccumulators();
+            for (final Map.Entry<Integer, TimelineSourceEventAccumulator> accumulatorEntry : hostAccumulators.entrySet()) {
+                final int categoryId = accumulatorEntry.getKey();
+                final TimelineSourceEventAccumulator accumulator = accumulatorEntry.getValue();
+                log.debug("Saving Timeline start time for sourceId [{}] and category [{}]", sourceId, categoryId);
+                startTimes.addTime(sourceId, categoryId, accumulator.getStartTime());
+            }
+        }
+    }
+
+    public synchronized void purgeOldSourcesAndAccumulators(final DateTime purgeIfBeforeDate) {
+        final List<Integer> oldSourceIds = new ArrayList<Integer>();
+        for (final Map.Entry<Integer, SourceAccumulatorsAndUpdateDate> entry : accumulators.entrySet()) {
+            final int sourceId = entry.getKey();
+            final SourceAccumulatorsAndUpdateDate accumulatorsAndDate = entry.getValue();
+            final DateTime lastUpdatedDate = accumulatorsAndDate.getLastUpdateDate();
+            if (lastUpdatedDate.isBefore(purgeIfBeforeDate)) {
+                oldSourceIds.add(sourceId);
+                purgedAccumsBecauseSourceNotUpdated.incrementAndGet();
+                for (final TimelineSourceEventAccumulator categoryAccumulator : accumulatorsAndDate.getCategoryAccumulators().values()) {
+                    categoryAccumulator.extractAndQueueTimelineChunks();
+                }
+            } else {
+                final List<Integer> categoryIdsToPurge = new ArrayList<Integer>();
+                final Map<Integer, TimelineSourceEventAccumulator> categoryMap = accumulatorsAndDate.getCategoryAccumulators();
+                for (final Map.Entry<Integer, TimelineSourceEventAccumulator> eventEntry : categoryMap.entrySet()) {
+                    final int categoryId = eventEntry.getKey();
+                    final TimelineSourceEventAccumulator categoryAccumulator = eventEntry.getValue();
+                    final DateTime latestTime = categoryAccumulator.getLatestSampleAddTime();
+                    if (latestTime != null && latestTime.isBefore(purgeIfBeforeDate)) {
+                        purgedAccumsBecauseCategoryNotUpdated.incrementAndGet();
+                        categoryAccumulator.extractAndQueueTimelineChunks();
+                        categoryIdsToPurge.add(categoryId);
+                    }
+                }
+                for (final int categoryId : categoryIdsToPurge) {
+                    categoryMap.remove(categoryId);
+                }
+            }
+        }
+        for (final int sourceIdToPurge : oldSourceIds) {
+            accumulators.remove(sourceIdToPurge);
+        }
+    }
+
+    /**
+     * Main entry point to the timeline subsystem. Record a series of sample for a given source, at a given timestamp.
+     *
+     * @param sourceName     name of the source
+     * @param eventType      event category
+     * @param eventTimestamp event timestamp
+     * @param samples        samples to record
+     */
+    public void record(final String sourceName, final String eventType, final DateTime eventTimestamp, final Map<String, Object> samples) {
+        if (shuttingDown.get()) {
+            eventsReceivedAfterShuttingDown.incrementAndGet();
+            return;
+        }
+        try {
+            handledEventCount.incrementAndGet();
+
+            // Find the sourceId
+            final int sourceId = timelineDAO.getOrAddSource(sourceName);
+
+            // Extract and parse samples
+            final Map<Integer, ScalarSample> scalarSamples = new LinkedHashMap<Integer, ScalarSample>();
+            convertSamplesToScalarSamples(sourceId, eventType, samples, scalarSamples);
+
+            if (scalarSamples.isEmpty()) {
+                eventsDiscarded.incrementAndGet();
+                return;
+            }
+
+            final SourceSamplesForTimestamp sourceSamples = new SourceSamplesForTimestamp(sourceId, eventType, eventTimestamp, scalarSamples);
+            if (!replaying.get()) {
+                // Start by saving locally the samples
+                backingBuffer.append(sourceSamples);
+            }
+            // Then add them to the in-memory accumulator
+            processSamples(sourceSamples);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public TimelineSourceEventAccumulator getOrAddSourceEventAccumulator(final int sourceId, final int categoryId, final DateTime firstSampleTime) {
+        return this.getOrAddSourceEventAccumulator(sourceId, categoryId, firstSampleTime, (int) config.getTimelineLength().getMillis());
+    }
+
+    public synchronized TimelineSourceEventAccumulator getOrAddSourceEventAccumulator(final int sourceId, final int categoryId, final DateTime firstSampleTime, final int timelineLengthMillis) {
+        SourceAccumulatorsAndUpdateDate sourceAccumulatorsAndUpdateDate = accumulators.get(sourceId);
+        if (sourceAccumulatorsAndUpdateDate == null) {
+            addedSourceEventAccumulatorMapCount.incrementAndGet();
+            sourceAccumulatorsAndUpdateDate = new SourceAccumulatorsAndUpdateDate(new HashMap<Integer, TimelineSourceEventAccumulator>(), new DateTime());
+            accumulators.put(sourceId, sourceAccumulatorsAndUpdateDate);
+        }
+        sourceAccumulatorsAndUpdateDate.markUpdated();
+        final Map<Integer, TimelineSourceEventAccumulator> hostCategoryAccumulators = sourceAccumulatorsAndUpdateDate.getCategoryAccumulators();
+        TimelineSourceEventAccumulator accumulator = hostCategoryAccumulators.get(categoryId);
+        if (accumulator == null) {
+            addedSourceEventAccumulatorCount.incrementAndGet();
+            accumulator = new TimelineSourceEventAccumulator(timelineDAO, timelineCoder, sampleCoder, backgroundWriter, sourceId, categoryId, firstSampleTime, timelineLengthMillis);
+            hostCategoryAccumulators.put(categoryId, accumulator);
+            log.debug("Created new Timeline for sourceId [{}] and category [{}]", sourceId, categoryId);
+        }
+        return accumulator;
+    }
+
+    @VisibleForTesting
+    public void processSamples(final SourceSamplesForTimestamp hostSamples) throws ExecutionException, IOException {
+        final int sourceId = hostSamples.getSourceId();
+        final String category = hostSamples.getCategory();
+        final int categoryId = timelineDAO.getEventCategoryId(category);
+        final DateTime timestamp = hostSamples.getTimestamp();
+        final TimelineSourceEventAccumulator accumulator = getOrAddSourceEventAccumulator(sourceId, categoryId, timestamp);
+        accumulator.addSourceSamples(hostSamples);
+    }
+
+    public Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+        return getInMemoryTimelineChunks(sourceId, ImmutableList.copyOf(timelineDAO.getMetricIdsBySourceId(sourceId)), filterStartTime, filterEndTime);
+    }
+
+    public Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, final Integer metricId, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+        return getInMemoryTimelineChunks(sourceId, ImmutableList.<Integer>of(metricId), filterStartTime, filterEndTime);
+    }
+
+    public synchronized Collection<? extends TimelineChunk> getInMemoryTimelineChunks(final Integer sourceId, final List<Integer> metricIds, @Nullable final DateTime filterStartTime, @Nullable final DateTime filterEndTime) throws IOException, ExecutionException {
+        getInMemoryChunksCallCount.incrementAndGet();
+        // Check first if there is an in-memory accumulator for this host
+        final SourceAccumulatorsAndUpdateDate sourceAccumulatorsAndDate = accumulators.get(sourceId);
+        if (sourceAccumulatorsAndDate == null) {
+            return ImmutableList.of();
+        }
+
+        // Now, filter each accumulator for this host
+        final List<TimelineChunk> samplesBySourceName = new ArrayList<TimelineChunk>();
+        for (final TimelineSourceEventAccumulator accumulator : sourceAccumulatorsAndDate.getCategoryAccumulators().values()) {
+            for (final TimelineChunk chunk : accumulator.getPendingTimelineChunks()) {
+                if ((filterStartTime != null && chunk.getEndTime().isBefore(filterStartTime)) ||
+                        (filterEndTime != null && chunk.getStartTime().isAfter(filterEndTime)) ||
+                        !metricIds.contains(chunk.getMetricId())) {
+                    continue;
+                } else {
+                    samplesBySourceName.add(chunk);
+                }
+            }
+            final List<DateTime> accumulatorTimes = accumulator.getTimes();
+            if (accumulatorTimes.size() == 0) {
+                continue;
+            }
+            final DateTime accumulatorStartTime = accumulator.getStartTime();
+            final DateTime accumulatorEndTime = accumulator.getEndTime();
+
+            // Check if the time filters apply
+            if ((filterStartTime != null && accumulatorEndTime.isBefore(filterStartTime)) || (filterEndTime != null && accumulatorStartTime.isAfter(filterEndTime))) {
+                // Ignore this accumulator
+                continue;
+            }
+
+            // This accumulator is in the right time range, now return only the sample kinds specified
+            final byte[] timeBytes = timelineCoder.compressDateTimes(accumulatorTimes);
+            for (final TimelineChunkAccumulator chunkAccumulator : accumulator.getTimelines().values()) {
+                if (metricIds.contains(chunkAccumulator.getMetricId())) {
+                    // Extract the timeline for this chunk by copying it and reading encoded bytes
+                    accumulatorDeepCopyCount.incrementAndGet();
+                    final TimelineChunkAccumulator chunkAccumulatorCopy = chunkAccumulator.deepCopy();
+                    final TimelineChunk timelineChunk = chunkAccumulatorCopy.extractTimelineChunkAndReset(accumulatorStartTime, accumulatorEndTime, timeBytes);
+                    samplesBySourceName.add(timelineChunk);
+                }
+            }
+        }
+        inMemoryChunksReturnedCount.addAndGet(samplesBySourceName.size());
+        Collections.sort(samplesBySourceName, CHUNK_COMPARATOR);
+        return samplesBySourceName;
+    }
+
+    @VisibleForTesting
+    void convertSamplesToScalarSamples(final Integer sourceId, final String eventType, final Map<String, Object> inputSamples, final Map<Integer, ScalarSample> outputSamples) {
+        if (inputSamples == null) {
+            return;
+        }
+        final Integer eventCategoryId = timelineDAO.getOrAddEventCategory(eventType);
+
+        for (final String attributeName : inputSamples.keySet()) {
+            final Integer metricId = timelineDAO.getOrAddMetric(sourceId, eventCategoryId, attributeName);
+            final Object sample = inputSamples.get(attributeName);
+
+            outputSamples.put(metricId, ScalarSample.fromObject(sample));
+        }
+    }
+
+    public void replay(final String spoolDir) {
+        replayCount.incrementAndGet();
+        log.info("Starting replay of files in {}", spoolDir);
+        final Replayer replayer = new Replayer(spoolDir);
+        StartTimes lastStartTimes = null;
+        if (shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES) {
+            lastStartTimes = timelineDAO.getLastStartTimes();
+            if (lastStartTimes == null) {
+                log.info("Did not find startTimes");
+            } else {
+                log.info("Retrieved startTimes from the db");
+            }
+        }
+        final StartTimes startTimes = lastStartTimes;
+        final DateTime minStartTime = lastStartTimes == null ? null : startTimes.getMinStartTime();
+        final long found = replaySamplesFoundCount.get();
+        final long outsideTimeRange = replaySamplesOutsideTimeRangeCount.get();
+        final long processed = replaySamplesProcessedCount.get();
+
+        try {
+            // Read all files in the spool directory and delete them after process, if
+            // startTimes  is null.
+            replaying.set(true);
+            final int filesSkipped = replayer.readAll(startTimes == null, minStartTime, new Function<SourceSamplesForTimestamp, Void>() {
+                @Override
+                public Void apply(@Nullable final SourceSamplesForTimestamp hostSamples) {
+                    if (hostSamples != null) {
+                        replaySamplesFoundCount.incrementAndGet();
+                        boolean useSamples = true;
+                        try {
+                            final int sourceId = hostSamples.getSourceId();
+                            final String category = hostSamples.getCategory();
+                            final int categoryId = timelineDAO.getEventCategoryId(category);
+                            // If startTimes is non-null and the samples come from before the first time for
+                            // the given host and event category, ignore the samples
+                            if (startTimes != null) {
+                                final DateTime timestamp = hostSamples.getTimestamp();
+                                final DateTime categoryStartTime = startTimes.getStartTimeForSourceIdAndCategoryId(sourceId, categoryId);
+                                if (timestamp == null ||
+                                        timestamp.isBefore(startTimes.getMinStartTime()) ||
+                                        (categoryStartTime != null && timestamp.isBefore(categoryStartTime))) {
+                                    replaySamplesOutsideTimeRangeCount.incrementAndGet();
+                                    useSamples = false;
+                                }
+                            }
+                            if (useSamples) {
+                                replaySamplesProcessedCount.incrementAndGet();
+                                processSamples(hostSamples);
+                            }
+                        } catch (Exception e) {
+                            log.warn("Got exception replaying sample, data potentially lost! {}", hostSamples.toString());
+                        }
+                    }
+
+                    return null;
+                }
+            });
+            if (shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES) {
+                timelineDAO.deleteLastStartTimes();
+                log.info("Deleted old startTimes");
+            }
+            log.info(String.format("Replay completed; %d files skipped, samples read %d, samples outside time range %d, samples used %d",
+                                   filesSkipped, replaySamplesFoundCount.get() - found, replaySamplesOutsideTimeRangeCount.get() - outsideTimeRange, replaySamplesProcessedCount.get() - processed));
+        } catch (RuntimeException e) {
+            // Catch the exception to make the collector start properly
+            log.error("Ignoring error when replaying the data", e);
+        } finally {
+            replaying.set(false);
+        }
+    }
+
+    public void forceCommit() {
+        forceCommitCallCount.incrementAndGet();
+        saveAccumulators();
+        backingBuffer.discard();
+        log.info("Timelines committed");
+    }
+
+    public void commitAndShutdown() {
+        shuttingDown.set(true);
+        final boolean doingFastShutdown = shutdownSaveMode == ShutdownSaveMode.SAVE_START_TIMES;
+        if (doingFastShutdown) {
+            final StartTimes startTimes = new StartTimes();
+            saveStartTimes(startTimes);
+            timelineDAO.insertLastStartTimes(startTimes);
+            log.info("During shutdown, saved timeline start times in the db");
+        } else {
+            saveAccumulators();
+            log.info("During shutdown, saved timeline accumulators");
+        }
+        performShutdown();
+        backingBuffer.discard();
+    }
+
+    private void performShutdown() {
+        backgroundWriter.initiateShutdown();
+        while (!backgroundWriter.getShutdownFinished()) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        purgeThread.shutdown();
+    }
+
+    private synchronized void purgeFilesAndAccumulators() {
+        this.purgeFilesAndAccumulators(new DateTime().minus(config.getTimelineLength().getMillis()), new DateTime().minus(2 * config.getTimelineLength().getMillis()));
+    }
+
+    // TODO: We have a bad interaction between startTimes and purging: If the system is down
+    // for two hours, we may not want it to purge everything.  Figure out what to do about this.
+    private synchronized void purgeFilesAndAccumulators(final DateTime purgeAccumulatorsIfBefore, final DateTime purgeFilesIfBefore) {
+        purgeOldSourcesAndAccumulators(purgeAccumulatorsIfBefore);
+        final Replayer replayer = new Replayer(config.getSpoolDir());
+        replayer.purgeOldFiles(purgeFilesIfBefore);
+    }
+
+    public void startHandlerThreads() {
+        purgeThread.scheduleWithFixedDelay(new Runnable() {
+            @Override
+            public void run() {
+                purgeFilesAndAccumulators();
+            }
+        },
+                                           config.getTimelineLength().getMillis(),
+                                           config.getTimelineLength().getMillis(),
+                                           TimeUnit.MILLISECONDS);
+    }
+
+    // We use the lastUpdateDate to purge sources and their accumulators from the map
+    private static class SourceAccumulatorsAndUpdateDate {
+
+        private final Map<Integer, TimelineSourceEventAccumulator> categoryAccumulators;
+        private DateTime lastUpdateDate;
+
+        public SourceAccumulatorsAndUpdateDate(final Map<Integer, TimelineSourceEventAccumulator> categoryAccumulators, final DateTime lastUpdateDate) {
+            this.categoryAccumulators = categoryAccumulators;
+            this.lastUpdateDate = lastUpdateDate;
+        }
+
+        public Map<Integer, TimelineSourceEventAccumulator> getCategoryAccumulators() {
+            return categoryAccumulators;
+        }
+
+        public DateTime getLastUpdateDate() {
+            return lastUpdateDate;
+        }
+
+        public void markUpdated() {
+            lastUpdateDate = new DateTime();
+        }
+    }
+
+    @VisibleForTesting
+    public Collection<TimelineSourceEventAccumulator> getAccumulators() {
+        final List<TimelineSourceEventAccumulator> inMemoryAccumulator = new ArrayList<TimelineSourceEventAccumulator>();
+        for (final SourceAccumulatorsAndUpdateDate sourceEventAccumulatorMap : accumulators.values()) {
+            inMemoryAccumulator.addAll(sourceEventAccumulatorMap.getCategoryAccumulators().values());
+        }
+
+        return inMemoryAccumulator;
+    }
+
+    @VisibleForTesting
+    public FileBackedBuffer getBackingBuffer() {
+        return backingBuffer;
+    }
+
+    public long getEventsDiscarded() {
+        return eventsDiscarded.get();
+    }
+
+    public long getSourceEventAccumulatorCount() {
+        return accumulators.size();
+    }
+
+    public long getEventsReceivedAfterShuttingDown() {
+        return eventsReceivedAfterShuttingDown.get();
+    }
+
+    public long getHandledEventCount() {
+        return handledEventCount.get();
+    }
+
+    public long getAddedSourceEventAccumulatorMapCount() {
+        return addedSourceEventAccumulatorMapCount.get();
+    }
+
+    public long getAddedSourceEventAccumulatorCount() {
+        return addedSourceEventAccumulatorCount.get();
+    }
+
+    public long getGetInMemoryChunksCallCount() {
+        return getInMemoryChunksCallCount.get();
+    }
+
+    public long getAccumulatorDeepCopyCount() {
+        return accumulatorDeepCopyCount.get();
+    }
+
+    public long getInMemoryChunksReturnedCount() {
+        return inMemoryChunksReturnedCount.get();
+    }
+
+    public long getReplayCount() {
+        return replayCount.get();
+    }
+
+    public long getReplaySamplesFoundCount() {
+        return replaySamplesFoundCount.get();
+    }
+
+    public long getReplaySamplesOutsideTimeRangeCount() {
+        return replaySamplesOutsideTimeRangeCount.get();
+    }
+
+    public long getReplaySamplesProcessedCount() {
+        return replaySamplesProcessedCount.get();
+    }
+
+    public long getForceCommitCallCount() {
+        return forceCommitCallCount.get();
+    }
+
+    public long getPurgedAccumsBecauseSourceNotUpdated() {
+        return purgedAccumsBecauseSourceNotUpdated.get();
+    }
+
+    public long getPurgedAccumsBecauseCategoryNotUpdated() {
+        return purgedAccumsBecauseCategoryNotUpdated.get();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java
new file mode 100644
index 0000000..d352dd9
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/TimelineSourceEventAccumulator.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.codec.SampleCoder;
+import com.ning.billing.usage.timeline.codec.TimelineChunkAccumulator;
+import com.ning.billing.usage.timeline.persistent.TimelineDao;
+import com.ning.billing.usage.timeline.samples.NullSample;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+/**
+ * This class represents a collection of timeline chunks, one for each
+ * metric belonging to one event category, each over a specific time period,
+ * from a single source.  This class is used to accumulate samples
+ * to be written to the database; a separate streaming class with
+ * much less overhead is used to "play back" the samples read from
+ * the db in response to queries.
+ * <p/>
+ * All subordinate timelines contain the same number of samples.
+ * <p/>
+ * When enough samples have accumulated, typically one hour's worth,
+ * in-memory samples are made into TimelineChunks, one chunk for each metricId
+ * maintained by the accumulator.
+ * <p/>
+ * These new chunks are organized as PendingChunkMaps, kept in a local list and also
+ * handed off to a PendingChunkMapConsumer to written to the db by a background process.  At some
+ * in the future, that background process will call markPendingChunkMapConsumed(),
+ * passing the id of a PendingChunkMap.  This causes the PendingChunkMap
+ * to be removed from the local list maintained by the TimelineSourceEventAccumulator.
+ * <p/>
+ * Queries that cause the TimelineSourceEventAccumulator instance to return memory
+ * chunks also return any chunks in PendingChunkMaps in the local list of pending chunks.
+ */
+public class TimelineSourceEventAccumulator {
+
+    private static final Logger log = LoggerFactory.getLogger(TimelineSourceEventAccumulator.class);
+    private static final DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime();
+    private static final NullSample nullSample = new NullSample();
+    private static final boolean checkEveryAccess = Boolean.parseBoolean(System.getProperty("killbill.usage.checkEveryAccess"));
+    private static final Random rand = new Random(0);
+
+    private final Map<Integer, SampleSequenceNumber> metricIdCounters = new HashMap<Integer, SampleSequenceNumber>();
+    private final List<PendingChunkMap> pendingChunkMaps = new ArrayList<PendingChunkMap>();
+    private long pendingChunkMapIdCounter = 1;
+
+    private final BackgroundDBChunkWriter backgroundWriter;
+    private final TimelineCoder timelineCoder;
+    private final SampleCoder sampleCoder;
+    private final Integer timelineLengthMillis;
+    private final int sourceId;
+    private final int eventCategoryId;
+    // This is the time when we want to end the chunk.  Setting the value randomly
+    // when the TimelineSourceEventAccumulator  is created provides a mechanism to
+    // distribute the db writes
+    private DateTime chunkEndTime = null;
+    private DateTime startTime = null;
+    private DateTime endTime = null;
+    private DateTime latestSampleAddTime;
+    private long sampleSequenceNumber = 0;
+    private int sampleCount = 0;
+
+    /**
+     * Maps the sample kind id to the accumulator for that sample kind
+     */
+    private final Map<Integer, TimelineChunkAccumulator> timelines = new ConcurrentHashMap<Integer, TimelineChunkAccumulator>();
+
+    /**
+     * Holds the sampling times of the samples
+     */
+    private final List<DateTime> times = new ArrayList<DateTime>();
+
+    public TimelineSourceEventAccumulator(final TimelineDao dao, final TimelineCoder timelineCoder, final SampleCoder sampleCoder,
+                                          final BackgroundDBChunkWriter backgroundWriter, final int sourceId, final int eventCategoryId,
+                                          final DateTime firstSampleTime, final Integer timelineLengthMillis) {
+        this.timelineLengthMillis = timelineLengthMillis;
+        this.backgroundWriter = backgroundWriter;
+        this.timelineCoder = timelineCoder;
+        this.sampleCoder = sampleCoder;
+        this.sourceId = sourceId;
+        this.eventCategoryId = eventCategoryId;
+        // Set the end-of-chunk time by tossing a random number, to evenly distribute the db writeback load.
+        this.chunkEndTime = timelineLengthMillis != null ? firstSampleTime.plusMillis(rand.nextInt(timelineLengthMillis)) : null;
+    }
+
+    /*
+     * This constructor is used for testing; it writes chunks as soon as they are
+     * created, but because the chunkEndTime is way in the future, doesn't initiate
+     * chunk writes.
+     */
+    public TimelineSourceEventAccumulator(final TimelineDao timelineDAO, final TimelineCoder timelineCoder, final SampleCoder sampleCoder,
+                                          final Integer sourceId, final int eventTypeId, final DateTime firstSampleTime) {
+        this(timelineDAO, timelineCoder, sampleCoder, new BackgroundDBChunkWriter(timelineDAO, null, true), sourceId, eventTypeId, firstSampleTime, Integer.MAX_VALUE);
+    }
+
+    @SuppressWarnings("unchecked")
+    // TODO - we can probably do better than synchronize the whole method
+    public synchronized void addSourceSamples(final SourceSamplesForTimestamp samples) {
+        final DateTime timestamp = samples.getTimestamp();
+
+        if (chunkEndTime != null && chunkEndTime.isBefore(timestamp)) {
+            extractAndQueueTimelineChunks();
+            startTime = timestamp;
+            chunkEndTime = timestamp.plusMillis(timelineLengthMillis);
+        }
+
+        if (startTime == null) {
+            startTime = timestamp;
+        }
+        if (endTime == null) {
+            endTime = timestamp;
+        } else if (!timestamp.isAfter(endTime)) {
+            log.warn("Adding samples for host {}, timestamp {} is not after the end time {}; ignored",
+                     new Object[]{sourceId, dateFormatter.print(timestamp), dateFormatter.print(endTime)});
+            return;
+        }
+        sampleSequenceNumber++;
+        latestSampleAddTime = new DateTime();
+        for (final Map.Entry<Integer, ScalarSample> entry : samples.getSamples().entrySet()) {
+            final Integer metricId = entry.getKey();
+            final SampleSequenceNumber counter = metricIdCounters.get(metricId);
+            if (counter != null) {
+                counter.setSequenceNumber(sampleSequenceNumber);
+            } else {
+                metricIdCounters.put(metricId, new SampleSequenceNumber(sampleSequenceNumber));
+            }
+            final ScalarSample sample = entry.getValue();
+            TimelineChunkAccumulator timeline = timelines.get(metricId);
+            if (timeline == null) {
+                timeline = new TimelineChunkAccumulator(sourceId, metricId, sampleCoder);
+                if (sampleCount > 0) {
+                    addPlaceholders(timeline, sampleCount);
+                }
+                timelines.put(metricId, timeline);
+            }
+            final ScalarSample compressedSample = sampleCoder.compressSample(sample);
+            timeline.addSample(compressedSample);
+        }
+        for (final Map.Entry<Integer, SampleSequenceNumber> entry : metricIdCounters.entrySet()) {
+            final SampleSequenceNumber counter = entry.getValue();
+            if (counter.getSequenceNumber() < sampleSequenceNumber) {
+                counter.setSequenceNumber(sampleSequenceNumber);
+                final int metricId = entry.getKey();
+                final TimelineChunkAccumulator timeline = timelines.get(metricId);
+                timeline.addSample(nullSample);
+            }
+        }
+        // Now we can update the state
+        endTime = timestamp;
+        sampleCount++;
+        times.add(timestamp);
+
+        if (checkEveryAccess) {
+            checkSampleCounts(sampleCount);
+        }
+    }
+
+    private void addPlaceholders(final TimelineChunkAccumulator timeline, int countToAdd) {
+        final int maxRepeatSamples = RepeatSample.MAX_SHORT_REPEAT_COUNT;
+        while (countToAdd >= maxRepeatSamples) {
+            timeline.addPlaceholder((byte) maxRepeatSamples);
+            countToAdd -= maxRepeatSamples;
+        }
+        if (countToAdd > 0) {
+            timeline.addPlaceholder((byte) countToAdd);
+        }
+    }
+
+    /**
+     * This method queues a map of TimelineChunks extracted from the TimelineChunkAccumulators
+     * to be written to the db.  When memory chunks are requested, any queued chunk will be included
+     * in the list.
+     */
+    public synchronized void extractAndQueueTimelineChunks() {
+        if (times.size() > 0) {
+            final Map<Integer, TimelineChunk> chunkMap = new HashMap<Integer, TimelineChunk>();
+            final byte[] timeBytes = timelineCoder.compressDateTimes(times);
+            for (final Map.Entry<Integer, TimelineChunkAccumulator> entry : timelines.entrySet()) {
+                final int metricId = entry.getKey();
+                final TimelineChunkAccumulator accumulator = entry.getValue();
+                final TimelineChunk chunk = accumulator.extractTimelineChunkAndReset(startTime, endTime, timeBytes);
+                chunkMap.put(metricId, chunk);
+            }
+            times.clear();
+            sampleCount = 0;
+            final long counter = pendingChunkMapIdCounter++;
+            final PendingChunkMap newChunkMap = new PendingChunkMap(this, counter, chunkMap);
+            pendingChunkMaps.add(newChunkMap);
+            backgroundWriter.addPendingChunkMap(newChunkMap);
+        }
+    }
+
+    public synchronized void markPendingChunkMapConsumed(final long pendingChunkMapId) {
+        final PendingChunkMap pendingChunkMap = pendingChunkMaps.size() > 0 ? pendingChunkMaps.get(0) : null;
+        if (pendingChunkMap == null) {
+            log.error("In TimelineSourceEventAccumulator.markPendingChunkMapConsumed(), could not find the map for {}", pendingChunkMapId);
+        } else if (pendingChunkMapId != pendingChunkMap.getPendingChunkMapId()) {
+            log.error("In TimelineSourceEventAccumulator.markPendingChunkMapConsumed(), the next map has id {}, but we're consuming id {}",
+                      pendingChunkMap.getPendingChunkMapId(), pendingChunkMapId);
+        } else {
+            pendingChunkMaps.remove(0);
+        }
+    }
+
+    public synchronized List<TimelineChunk> getPendingTimelineChunks() {
+        final List<TimelineChunk> timelineChunks = new ArrayList<TimelineChunk>();
+        for (final PendingChunkMap pendingChunkMap : pendingChunkMaps) {
+            timelineChunks.addAll(pendingChunkMap.getChunkMap().values());
+        }
+        return timelineChunks;
+    }
+
+    /**
+     * Make sure all timelines have the sample count passed in; otherwise log
+     * discrepancies and return false
+     *
+     * @param assertedCount The sample count that all timelines are supposed to have
+     * @return true if all timelines have the right count; false otherwise
+     */
+    public boolean checkSampleCounts(final int assertedCount) {
+        boolean success = true;
+        if (assertedCount != sampleCount) {
+            log.error("For host {}, start time {}, the SourceTimeLines sampleCount {} is not equal to the assertedCount {}",
+                      new Object[]{sourceId, dateFormatter.print(startTime), sampleCount, assertedCount});
+            success = false;
+        }
+        for (final Map.Entry<Integer, TimelineChunkAccumulator> entry : timelines.entrySet()) {
+            final int metricId = entry.getKey();
+            final TimelineChunkAccumulator timeline = entry.getValue();
+            final int lineSampleCount = timeline.getSampleCount();
+            if (lineSampleCount != assertedCount) {
+                log.error("For host {}, start time {}, sample kind id {}, the sampleCount {} is not equal to the assertedCount {}",
+                          new Object[]{sourceId, dateFormatter.print(startTime), metricId, lineSampleCount, assertedCount});
+                success = false;
+            }
+        }
+        return success;
+    }
+
+    public int getSourceId() {
+        return sourceId;
+    }
+
+    public int getEventCategoryId() {
+        return eventCategoryId;
+    }
+
+    public DateTime getStartTime() {
+        return startTime;
+    }
+
+    public DateTime getEndTime() {
+        return endTime;
+    }
+
+    public Map<Integer, TimelineChunkAccumulator> getTimelines() {
+        return timelines;
+    }
+
+    public List<DateTime> getTimes() {
+        return times;
+    }
+
+    public DateTime getLatestSampleAddTime() {
+        return latestSampleAddTime;
+    }
+
+    private static class SampleSequenceNumber {
+
+        private long sequenceNumber;
+
+        public SampleSequenceNumber(final long sequenceNumber) {
+            this.sequenceNumber = sequenceNumber;
+        }
+
+        public long getSequenceNumber() {
+            return sequenceNumber;
+        }
+
+        public void setSequenceNumber(final long sequenceNumber) {
+            this.sequenceNumber = sequenceNumber;
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java
new file mode 100644
index 0000000..620b7e0
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCoder.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class DefaultTimelineCoder implements TimelineCoder {
+
+    public static final Logger log = LoggerFactory.getLogger(TimelineCoder.class);
+    public static final int MAX_SHORT_REPEAT_COUNT = 0xFFFF;
+    public static final int MAX_BYTE_REPEAT_COUNT = 0xFF;
+
+    /**
+     * Convert the array of unix times to a compressed timeline, and return the byte array
+     * representing that compressed timeline
+     *
+     * @param times an int array giving the unix times to be compressed
+     * @return the compressed timeline
+     */
+    @Override
+    public byte[] compressDateTimes(final List<DateTime> times) {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final DataOutputStream dataStream = new DataOutputStream(outputStream);
+        try {
+            int lastTime = 0;
+            int lastDelta = 0;
+            int repeatCount = 0;
+            for (final DateTime time : times) {
+                final int newTime = DateTimeUtils.unixSeconds(time);
+                if (lastTime == 0) {
+                    lastTime = newTime;
+                    writeTime(0, lastTime, dataStream);
+                    continue;
+                } else if (newTime < lastTime) {
+                    log.warn("In TimelineCoder.compressTimes(), newTime {} is < lastTime {}; ignored", newTime, lastTime);
+                    continue;
+                }
+                final int delta = newTime - lastTime;
+                final boolean deltaWorks = delta <= TimelineOpcode.MAX_DELTA_TIME;
+                final boolean sameDelta = repeatCount > 0 && delta == lastDelta;
+                if (deltaWorks) {
+                    if (sameDelta) {
+                        repeatCount++;
+                        if (repeatCount == MAX_SHORT_REPEAT_COUNT) {
+                            writeRepeatedDelta(delta, repeatCount, dataStream);
+                            repeatCount = 0;
+                        }
+                    } else {
+                        if (repeatCount > 0) {
+                            writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+                        }
+                        repeatCount = 1;
+                    }
+                    lastDelta = delta;
+                } else {
+                    if (repeatCount > 0) {
+                        writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+                    }
+                    writeTime(0, newTime, dataStream);
+                    repeatCount = 0;
+                    lastDelta = 0;
+                }
+                lastTime = newTime;
+            }
+            if (repeatCount > 0) {
+                writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+            }
+            dataStream.flush();
+            return outputStream.toByteArray();
+        } catch (IOException e) {
+            log.error("Exception compressing times list of length {}", times.size(), e);
+            return null;
+        }
+    }
+
+    @Override
+    public byte[] combineTimelines(final List<byte[]> timesList, final Integer sampleCount) {
+        final byte[] timeBytes = combineTimelines(timesList);
+        final int combinedSampleCount = countTimeBytesSamples(timeBytes);
+        if (sampleCount != null && sampleCount != combinedSampleCount) {
+            final StringBuilder builder = new StringBuilder();
+            builder.append("In compressTimelineTimes(), combined sample count is ")
+                   .append(combinedSampleCount)
+                   .append(", but sample count is ")
+                   .append(sampleCount)
+                   .append(", combined TimeBytes ")
+                   .append(Hex.encodeHex(timeBytes))
+                   .append(", ")
+                   .append(timesList.size())
+                   .append(" chunks");
+            for (final byte[] bytes : timesList) {
+                builder.append(", ")
+                       .append(Hex.encodeHex(bytes));
+            }
+            log.error(builder.toString());
+        }
+        return timeBytes;
+    }
+
+    private byte[] combineTimelines(final List<byte[]> timesList) {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final DataOutputStream dataStream = new DataOutputStream(outputStream);
+        try {
+            int lastTime = 0;
+            int lastDelta = 0;
+            int repeatCount = 0;
+            int chunkCounter = 0;
+            for (byte[] times : timesList) {
+                final ByteArrayInputStream byteStream = new ByteArrayInputStream(times);
+                final DataInputStream byteDataStream = new DataInputStream(byteStream);
+                int byteCursor = 0;
+                while (true) {
+                    // Part 1: Get the opcode, and come up with newTime, newCount and newDelta
+                    final int opcode = byteDataStream.read();
+                    if (opcode == -1) {
+                        break;
+                    }
+                    byteCursor++;
+                    int newTime = 0;
+                    int newCount = 0;
+                    int newDelta = 0;
+                    boolean useNewDelta = false;
+                    boolean nonDeltaTime = false;
+                    if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+                        newTime = byteDataStream.readInt();
+                        if (newTime < lastTime) {
+                            log.warn("In TimelineCoder.combineTimeLines(), the fulltime read is %d, but the lastTime is %d; setting newTime to lastTime",
+                                     newTime, lastTime);
+                            newTime = lastTime;
+                        }
+                        byteCursor += 4;
+                        if (lastTime == 0) {
+                            writeTime(0, newTime, dataStream);
+                            lastTime = newTime;
+                            lastDelta = 0;
+                            repeatCount = 0;
+                            continue;
+                        } else if (newTime - lastTime <= TimelineOpcode.MAX_DELTA_TIME) {
+                            newDelta = newTime - lastTime;
+                            useNewDelta = true;
+                            newCount = 1;
+                        } else {
+                            nonDeltaTime = true;
+                        }
+                    } else if (opcode <= TimelineOpcode.MAX_DELTA_TIME) {
+                        newTime = lastTime + opcode;
+                        newDelta = opcode;
+                        useNewDelta = true;
+                        newCount = 1;
+                    } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+                        newCount = byteDataStream.read();
+                        newDelta = byteDataStream.read();
+                        useNewDelta = true;
+                        byteCursor += 2;
+                        if (lastTime != 0) {
+                            newTime = lastTime + newDelta * newCount;
+                        } else {
+                            throw new IllegalStateException(String.format("In TimelineCoder.combineTimelines, lastTime is 0 byte opcode = %d, byteCursor %d, chunkCounter %d, chunk %s",
+                                                                          opcode, byteCursor, chunkCounter, new String(Hex.encodeHex(times))));
+                        }
+                    } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+                        newCount = byteDataStream.readUnsignedShort();
+                        newDelta = byteDataStream.read();
+                        useNewDelta = true;
+                        byteCursor += 3;
+                        if (lastTime != 0) {
+                            newTime = lastTime + newDelta * newCount;
+                        }
+                    } else {
+                        throw new IllegalStateException(String.format("In TimelineCoder.combineTimelines, Unrecognized byte opcode = %d, byteCursor %d, chunkCounter %d, chunk %s",
+                                                                      opcode, byteCursor, chunkCounter, new String(Hex.encodeHex(times))));
+                    }
+                    // Part 2: Combine existing state represented in lastTime, lastDelta and repeatCount with newTime, newCount and newDelta
+                    if (lastTime == 0) {
+                        log.error("In combineTimelines(), lastTime is 0; byteCursor {}, chunkCounter {}, times {}", new Object[]{byteCursor, chunkCounter, new String(Hex.encodeHex(times))});
+                    } else if (repeatCount > 0) {
+                        if (lastDelta == newDelta && newCount > 0) {
+                            repeatCount += newCount;
+                            lastTime = newTime;
+                        } else {
+                            writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+                            if (useNewDelta) {
+                                lastDelta = newDelta;
+                                repeatCount = newCount;
+                                lastTime = newTime;
+                            } else {
+                                writeTime(lastTime, newTime, dataStream);
+                                lastTime = newTime;
+                                lastDelta = 0;
+                                repeatCount = 0;
+                            }
+                        }
+                    } else if (nonDeltaTime) {
+                        writeTime(lastTime, newTime, dataStream);
+                        lastTime = newTime;
+                        lastDelta = 0;
+                        repeatCount = 0;
+                    } else if (lastDelta == 0) {
+                        lastTime = newTime;
+                        repeatCount = newCount;
+                        lastDelta = newDelta;
+                    }
+                }
+                chunkCounter++;
+            }
+
+            if (repeatCount > 0) {
+                writeRepeatedDelta(lastDelta, repeatCount, dataStream);
+            }
+            dataStream.flush();
+
+            return outputStream.toByteArray();
+        } catch (Exception e) {
+            log.error("In combineTimesLines(), exception combining timelines", e);
+            return new byte[0];
+        }
+    }
+
+    @Override
+    public List<DateTime> decompressDateTimes(final byte[] compressedTimes) {
+        final List<DateTime> dateTimeList = new ArrayList<DateTime>(compressedTimes.length * 4);
+        final ByteArrayInputStream byteStream = new ByteArrayInputStream(compressedTimes);
+        final DataInputStream byteDataStream = new DataInputStream(byteStream);
+        int opcode;
+        int lastTime = 0;
+        try {
+            while (true) {
+                opcode = byteDataStream.read();
+                if (opcode == -1) {
+                    break;
+                }
+
+                if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+                    lastTime = byteDataStream.readInt();
+                    dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+                } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+                    final int repeatCount = byteDataStream.readUnsignedByte();
+                    final int delta = byteDataStream.readUnsignedByte();
+                    for (int i = 0; i < repeatCount; i++) {
+                        lastTime = lastTime + delta;
+                        dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+                    }
+                } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+                    final int repeatCount = byteDataStream.readUnsignedShort();
+                    final int delta = byteDataStream.readUnsignedByte();
+                    for (int i = 0; i < repeatCount; i++) {
+                        lastTime = lastTime + delta;
+                        dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+                    }
+                } else {
+                    // The opcode is itself a singleton delta
+                    lastTime = lastTime + opcode;
+                    dateTimeList.add(DateTimeUtils.dateTimeFromUnixSeconds(lastTime));
+                }
+            }
+        } catch (IOException e) {
+            log.error("In decompressTimes(), exception decompressing", e);
+        }
+
+        return dateTimeList;
+    }
+
+    @Override
+    public int countTimeBytesSamples(final byte[] timeBytes) {
+        int count = 0;
+        try {
+            final ByteArrayInputStream byteStream = new ByteArrayInputStream(timeBytes);
+            final DataInputStream byteDataStream = new DataInputStream(byteStream);
+            int opcode;
+            while ((opcode = byteDataStream.read()) != -1) {
+                if (opcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+                    byteDataStream.readInt();
+                    count++;
+                } else if (opcode <= TimelineOpcode.MAX_DELTA_TIME) {
+                    count++;
+                } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+                    count += byteDataStream.read();
+                    //noinspection ResultOfMethodCallIgnored
+                    byteDataStream.read();
+                } else if (opcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+                    count += byteDataStream.readUnsignedShort();
+                    //noinspection ResultOfMethodCallIgnored
+                    byteDataStream.read();
+                } else {
+                    throw new IllegalStateException(String.format("In TimelineCoder.countTimeBytesSamples(), unrecognized opcode %d", opcode));
+                }
+            }
+            return count;
+        } catch (IOException e) {
+            log.error("IOException while counting timeline samples", e);
+            return count;
+        }
+    }
+
+    private void writeRepeatedDelta(final int delta, final int repeatCount, final DataOutputStream dataStream) throws IOException {
+        if (repeatCount > 1) {
+            if (repeatCount > MAX_BYTE_REPEAT_COUNT) {
+                dataStream.writeByte(TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex());
+                dataStream.writeShort(repeatCount);
+            } else if (repeatCount == 2) {
+                dataStream.writeByte(delta);
+            } else {
+                dataStream.writeByte(TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+                dataStream.writeByte(repeatCount);
+            }
+        }
+        dataStream.writeByte(delta);
+    }
+
+    private void writeTime(final int lastTime, final int newTime, final DataOutputStream dataStream) throws IOException {
+        if (newTime > lastTime) {
+            final int delta = (newTime - lastTime);
+            if (delta <= TimelineOpcode.MAX_DELTA_TIME) {
+                dataStream.writeByte(delta);
+            } else {
+                dataStream.writeByte(TimelineOpcode.FULL_TIME.getOpcodeIndex());
+                dataStream.writeInt(newTime);
+            }
+        } else if (newTime == lastTime) {
+            dataStream.writeByte(0);
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java
new file mode 100644
index 0000000..4831e5c
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/DefaultTimelineCursor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+
+public class DefaultTimelineCursor implements TimelineCursor {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultTimelineCursor.class);
+
+    private final DataInputStream timelineDataStream;
+    private int sampleCount;
+    private int sampleNumber;
+    private int byteCursor;
+    private int lastValue;
+    private int delta;
+    private int repeatCount;
+
+    public DefaultTimelineCursor(final byte[] times, final int sampleCount) {
+        this.timelineDataStream = new DataInputStream(new ByteArrayInputStream(times));
+        this.sampleCount = sampleCount;
+        this.sampleNumber = 0;
+        this.byteCursor = 0;
+        this.lastValue = 0;
+        this.delta = 0;
+        this.repeatCount = 0;
+    }
+
+    private int getNextTimeInternal() {
+        try {
+            if (repeatCount > 0) {
+                repeatCount--;
+                lastValue += delta;
+            } else {
+                final int nextOpcode = timelineDataStream.read();
+                byteCursor++;
+                if (nextOpcode == -1) {
+                    return nextOpcode;
+                }
+                if (nextOpcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+                    lastValue = timelineDataStream.readInt();
+                    byteCursor += 4;
+                } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+                    repeatCount = timelineDataStream.readUnsignedByte() - 1;
+                    delta = timelineDataStream.read();
+                    byteCursor += 2;
+                    lastValue += delta;
+                } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+                    repeatCount = timelineDataStream.readUnsignedShort() - 1;
+                    delta = timelineDataStream.read();
+                    byteCursor += 3;
+                    lastValue += delta;
+                } else if (nextOpcode <= TimelineOpcode.MAX_DELTA_TIME) {
+                    lastValue += nextOpcode;
+                } else {
+                    throw new IllegalStateException(String.format("In TimeIterator.getNextTime(), unknown opcode %x at offset %d", nextOpcode, byteCursor));
+                }
+            }
+            sampleNumber++;
+            if (sampleNumber > sampleCount) {
+                log.error("In TimeIterator.getNextTime(), after update, sampleNumber %d > sampleCount %d", sampleNumber, sampleCount);
+            }
+            return lastValue;
+        } catch (IOException e) {
+            log.error("IOException in TimeIterator.getNextTime()", e);
+            return -1;
+        }
+    }
+
+    @Override
+    public void skipToSampleNumber(final int finalSampleNumber) {
+        if (finalSampleNumber > sampleCount) {
+            log.error("In TimeIterator.skipToSampleNumber(), finalSampleCount {} > sampleCount {}", finalSampleNumber, sampleCount);
+        }
+        while (sampleNumber < finalSampleNumber) {
+            try {
+                if (repeatCount > 0) {
+                    final int countToSkipInRepeat = Math.min(finalSampleNumber - sampleNumber, repeatCount);
+                    sampleNumber += countToSkipInRepeat;
+                    repeatCount -= countToSkipInRepeat;
+                    lastValue += countToSkipInRepeat * delta;
+                } else {
+                    final int nextOpcode = timelineDataStream.read();
+                    if (nextOpcode == -1) {
+                        return;
+                    }
+                    byteCursor++;
+                    if (nextOpcode == TimelineOpcode.FULL_TIME.getOpcodeIndex()) {
+                        lastValue = timelineDataStream.readInt();
+                        byteCursor += 4;
+                        sampleNumber++;
+                    } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex()) {
+                        repeatCount = timelineDataStream.readUnsignedByte() - 1;
+                        delta = timelineDataStream.read();
+                        byteCursor += 2;
+                        lastValue += delta;
+                        sampleNumber++;
+                    } else if (nextOpcode == TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex()) {
+                        repeatCount = timelineDataStream.readUnsignedShort() - 1;
+                        delta = timelineDataStream.read();
+                        byteCursor += 3;
+                        lastValue += delta;
+                        sampleNumber++;
+                    } else if (nextOpcode <= TimelineOpcode.MAX_DELTA_TIME) {
+                        lastValue += nextOpcode;
+                        sampleNumber++;
+                    } else {
+                        throw new IllegalStateException(String.format("In TimeIterator.skipToSampleNumber(), unknown opcode %x at offset %d", nextOpcode, byteCursor));
+                    }
+                }
+            } catch (IOException e) {
+                log.error("IOException in TimeIterator.getNextTime()", e);
+            }
+        }
+    }
+
+    @Override
+    public DateTime getNextTime() {
+        final int nextTime = getNextTimeInternal();
+        if (nextTime == -1) {
+            throw new IllegalStateException(String.format("In DecodedSampleOutputProcessor.getNextTime(), got -1 from timeCursor.getNextTimeInternal()"));
+        } else {
+            return DateTimeUtils.dateTimeFromUnixSeconds(nextTime);
+        }
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java
new file mode 100644
index 0000000..f010417
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCoder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import java.util.List;
+
+import org.joda.time.DateTime;
+
+public interface TimelineCoder {
+
+    /**
+     * Compress the list of DateTimes, producing the bytes of a timeline
+     *
+     * @param dateTimes a list of DateTimes to compress
+     * @return the bytes of the resulting timeline
+     */
+    public byte[] compressDateTimes(final List<DateTime> dateTimes);
+
+    /**
+     * Decompress the timeline bytes argument, returning a list of DateTimes
+     * Currently only used by tests.
+     *
+     * @param compressedTimes the timeline bytes
+     * @return a list of DateTimes representing the timeline times
+     */
+    public List<DateTime> decompressDateTimes(final byte[] compressedTimes);
+
+    /**
+     * Recode and combine the list of timeline byte objects, returning a single timeline.
+     * If the sampleCount is non-null and is not equal to the sum of the sample counts
+     * of the list of timelines, throw an error
+     *
+     * @param timesList   a list of timeline byte arrays
+     * @param sampleCount the expected count of samples for all timeline byte arrays
+     * @return the combined timeline
+     */
+    public byte[] combineTimelines(final List<byte[]> timesList, final Integer sampleCount);
+
+    /**
+     * Return a count of the time samples in the timeline provided
+     *
+     * @param timeBytes the bytes of a timeline
+     * @return the count of samples represented in the timeline
+     */
+    public int countTimeBytesSamples(final byte[] timeBytes);
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java
new file mode 100644
index 0000000..474526a
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineCursor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import org.joda.time.DateTime;
+
+public interface TimelineCursor {
+
+    /**
+     * Skip to the given sample number within the timeline, where 0 is the first sample number
+     *
+     * @param finalSampleNumber the sample number to skip to
+     */
+    public void skipToSampleNumber(final int finalSampleNumber);
+
+    /**
+     * Return the DateTime for the next sample
+     *
+     * @return the DateTime for the next sample.  If we've run out of samples, return null
+     */
+    public DateTime getNextTime();
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java
new file mode 100644
index 0000000..73b3420
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/times/TimelineOpcode.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+/**
+ * Opcodes are 1-byte entities.  Any "opcode" whose value is 240 or less is treated
+ * as a time delta to be added to the previous time value.
+ */
+public enum TimelineOpcode {
+    FULL_TIME(0xFF),                 // Followed by 4 bytes of int value
+    REPEATED_DELTA_TIME_BYTE(0xFE),  // Followed by a byte repeat count byte, 1-255, and then by a 1-byte delta whose value is 1-240
+    REPEATED_DELTA_TIME_SHORT(0xFD); // Followed by a repeat count short, 1-65535, and then by a 1-byte delta whose value is 1-240
+
+    public static final int MAX_DELTA_TIME = 0xF0;      // 240: Leaves room for 16 other opcodes, of which 3 are used
+
+    private final int opcodeIndex;
+
+    private TimelineOpcode(final int opcodeIndex) {
+        this.opcodeIndex = opcodeIndex;
+    }
+
+    public int getOpcodeIndex() {
+        return opcodeIndex;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("TimelineOpcode");
+        sb.append("{opcodeIndex=").append(opcodeIndex);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java b/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java
new file mode 100644
index 0000000..c5222d7
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/util/DateTimeUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.util;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+public class DateTimeUtils {
+
+    public static DateTime dateTimeFromUnixSeconds(final int unixTime) {
+        return new DateTime(((long) unixTime) * 1000L, DateTimeZone.UTC);
+    }
+
+    public static int unixSeconds(final DateTime dateTime) {
+        final long millis = dateTime.toDateTime(DateTimeZone.UTC).getMillis();
+        return (int) (millis / 1000L);
+    }
+}
diff --git a/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java b/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java
new file mode 100644
index 0000000..032b943
--- /dev/null
+++ b/usage/src/main/java/com/ning/billing/usage/timeline/util/Hex.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.util;
+
+/**
+ * Hex utilities from commons-codec
+ */
+public class Hex {
+
+    /**
+     * Used to build output as Hex
+     */
+    private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+    /**
+     * Used to build output as Hex
+     */
+    private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    private Hex() {
+    }
+
+    /**
+     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data a byte[] to convert to Hex characters
+     * @return A char[] containing hexadecimal characters
+     */
+    public static char[] encodeHex(final byte[] data) {
+        return encodeHex(data, true);
+    }
+
+    /**
+     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data        a byte[] to convert to Hex characters
+     * @param toLowerCase <code>true</code> converts to lowercase, <code>false</code> to uppercase
+     * @return A char[] containing hexadecimal characters
+     * @since 1.4
+     */
+    public static char[] encodeHex(final byte[] data, final boolean toLowerCase) {
+        return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+    }
+
+    /**
+     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
+     * The returned array will be double the length of the passed array, as it takes two characters to represent any
+     * given byte.
+     *
+     * @param data     a byte[] to convert to Hex characters
+     * @param toDigits the output alphabet
+     * @return A char[] containing hexadecimal characters
+     * @since 1.4
+     */
+    public static char[] encodeHex(final byte[] data, final char[] toDigits) {
+        final int l = data.length;
+        final char[] out = new char[l << 1];
+        // two characters form the hex value.
+        for (int i = 0, j = 0; i < l; i++) {
+            out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+            out[j++] = toDigits[0x0F & data[i]];
+        }
+
+        return out;
+    }
+
+    /**
+     * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The
+     * returned array will be half the length of the passed array, as it takes two characters to represent any given
+     * byte. An exception is thrown if the passed char array has an odd number of elements.
+     *
+     * @param data An array of characters containing hexadecimal digits
+     * @return A byte array containing binary data decoded from the supplied char array.
+     * @throws IllegalArgumentException Thrown if an odd number or illegal of characters is supplied
+     */
+    public static byte[] decodeHex(final char[] data) throws IllegalArgumentException {
+        final int len = data.length;
+        if ((len & 0x01) != 0) {
+            throw new IllegalArgumentException("Odd number of characters.");
+        }
+
+        final byte[] out = new byte[len >> 1];
+
+        // two characters form the hex value.
+        for (int i = 0, j = 0; j < len; i++) {
+            int f = toDigit(data[j], j) << 4;
+            j++;
+            f = f | toDigit(data[j], j);
+            j++;
+            out[i] = (byte) (f & 0xFF);
+        }
+
+        return out;
+    }
+
+    /**
+     * Converts a hexadecimal character to an integer.
+     *
+     * @param ch    A character to convert to an integer digit
+     * @param index The index of the character in the source
+     * @return An integer
+     * @throws IllegalArgumentException Thrown if ch is an illegal hex character
+     */
+    private static int toDigit(final char ch, final int index) throws IllegalArgumentException {
+        final int digit = Character.digit(ch, 16);
+        if (digit == -1) {
+            throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
+        }
+        return digit;
+    }
+}
diff --git a/usage/src/main/resources/com/ning/billing/usage/ddl.sql b/usage/src/main/resources/com/ning/billing/usage/ddl.sql
new file mode 100644
index 0000000..fab0ce2
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/ddl.sql
@@ -0,0 +1,67 @@
+create table sources (
+  record_id int(11) unsigned not null auto_increment
+, bundle_id char(36) default null
+, subscription_id char(36) default null
+, created_date datetime default null
+, created_by varchar(50) default null
+, updated_date datetime default null
+, updated_by varchar(50) default null
+, account_record_id int(11) unsigned default null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, index created_dt_record_id_dx (created_dt, record_id)
+) engine = innodb default charset = latin1;
+
+create table event_categories (
+  event_category_id integer not null auto_increment
+, event_category varchar(256) not null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, unique index event_category_unq (event_category)
+) engine = innodb default charset = latin1;
+
+create table metrics (
+  record_id int(11) unsigned not null auto_increment
+, event_category_id integer not null
+, metric varchar(256) not null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+, unique index metric_unq (event_category_id, metric)
+) engine = innodb default charset = latin1;
+
+create table timeline_chunks (
+  record_id bigint not null auto_increment
+, source_id integer not null
+, metric_id integer not null
+, sample_count integer not null
+, start_time integer not null
+, end_time integer not null
+, not_valid tinyint default 0
+, aggregation_level tinyint default 0
+, dont_aggregate tinyint default 0
+, in_row_samples varbinary(400) default null
+, blob_samples mediumblob default null
+, primary key(record_id)
+, unique index source_id_timeline_chunk_metric_idx (source_id, metric_id, start_time, aggregation_level)
+, index valid_agg_host_start_time (not_valid, aggregation_level, source_id, metric_id, start_time)
+) engine = innodb default charset = latin1;
+
+create table last_start_times (
+  time_inserted int not null primary key
+, start_times mediumtext not null
+) engine = innodb default charset = latin1;
+
+insert ignore into timeline_chunks(chunk_id, source_id, metric_id, sample_count, start_time, end_time, in_row_samples, blob_samples)
+                           values (0, 0, 0, 0, 0, 0, null, null);
+
+create table timeline_rolled_up_chunk (
+  record_id bigint not null auto_increment
+, source_id integer not null
+, metric_id integer not null
+, start_time date not null
+, end_time date not null
+, value bigint not null
+, account_record_id int(11) unsigned default null
+, tenant_record_id int(11) unsigned default null
+, primary key(record_id)
+) engine = innodb default charset = latin1;
diff --git a/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg b/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg
new file mode 100644
index 0000000..341ea3a
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/timeline/aggregator/TimelineAggregatorSqlDao.sql.stg
@@ -0,0 +1,60 @@
+group TimelineAggregatorDAO;
+
+getStreamingAggregationCandidates() ::= <<
+  select
+    chunk_id
+  , source_id
+  , metric_id
+  , start_time
+  , end_time
+  , in_row_samples
+  , blob_samples
+  , sample_count
+  , aggregation_level
+  , not_valid
+  , dont_aggregate
+  from timeline_chunks
+  where source_id != 0 and aggregation_level = :aggregationLevel and not_valid = 0
+  order by source_id, metric_id, start_time
+ >>
+
+ getAggregationCandidatesForSourceIdAndMetricIds(metricIds) ::= <<
+  select
+    chunk_id
+  , source_id
+  , metric_id
+  , start_time
+  , end_time
+  , in_row_samples
+  , blob_samples
+  , sample_count
+  , aggregation_level
+  , not_valid
+  , dont_aggregate
+  from timeline_chunks
+  where source_id = :source_id
+  and metric_id in (<metricIds>)
+  ;
+>>
+
+getLastInsertedId() ::= <<
+  select last_insert_id();
+>>
+
+makeTimelineChunkValid() ::= <<
+  update timeline_chunks
+  set not_valid = 0
+  where chunk_id = :chunkId
+  ;
+>>
+
+makeTimelineChunksInvalid(chunkIds) ::=<<
+  update timeline_chunks
+  set not_valid = 1
+  where chunk_id in (<chunkIds>)
+  ;
+>>
+
+deleteTimelineChunks(chunkIds) ::=<<
+  delete from timeline_chunks where chunk_id in (<chunkIds>);
+>>
diff --git a/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg b/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg
new file mode 100644
index 0000000..1e0364b
--- /dev/null
+++ b/usage/src/main/resources/com/ning/billing/usage/timeline/persistent/TimelineSqlDao.sql.stg
@@ -0,0 +1,181 @@
+group TimelineSqlDao;
+
+getSource() ::= <<
+  select
+    source_name
+  from sources
+  where source_id = :sourceId
+  ;
+>>
+
+getSources() ::= <<
+  select
+    source_id
+  , source_name
+  from sources
+  ;
+>>
+
+addSource() ::= <<
+  insert ignore into sources (source_name, created_dt)
+  values (:sourceName, unix_timestamp());
+>>
+
+getEventCategories() ::= <<
+  select event_category_id, event_category
+  from event_categories
+  order by event_category_id asc
+  ;
+>>
+
+getEventCategoryId() ::= <<
+  select
+    event_category_id
+  from event_categories
+  where event_category = :eventCategory
+  ;
+>>
+
+getEventCategory() ::= <<
+  select
+    event_category
+  from event_categories
+  where event_category_id = :eventCategoryId
+  ;
+>>
+
+addEventCategory() ::= <<
+  insert ignore into event_categories (event_category)
+  values (:eventCategory);
+>>
+
+getMetricId() ::= <<
+  select
+    metric_id
+  from metrics
+  where metric = :metric
+    and event_category_id = :eventCategoryId
+  ;
+>>
+
+getEventCategoryIdAndMetric() ::= <<
+  select
+    event_category_id
+  , metric
+  from metrics
+  where metric_id = :metricId
+  ;
+>>
+
+getMetric() ::= <<
+  select
+    metric
+  from metrics
+  where metric_id = :metricId
+  ;
+>>
+
+addMetric() ::= <<
+  insert ignore into metrics (event_category_id, metric)
+  values (:eventCategoryId, :metric);
+>>
+
+getMetricIdsBySourceId() ::= <<
+  select distinct metric_id
+  from timeline_chunks c
+  where source_id = :sourceId
+  ;
+>>
+
+getMetricIdsForAllSources() ::= <<
+  select distinct metric_id, source_id
+  from timeline_chunks c
+  ;
+>>
+
+getMetrics() ::= <<
+  select
+    metric_id
+  , event_category_id
+  , metric
+  from metrics
+  ;
+>>
+
+getLastInsertedId() ::= <<
+  select last_insert_id();
+>>
+
+insertTimelineChunk() ::= <<
+  insert into timeline_chunks (record_id, source_id, metric_id, sample_count, start_time, end_time, in_row_samples, blob_samples, aggregation_level, not_valid, dont_aggregate)
+  values (:chunkId, :sourceId, :metricId, :sampleCount, :startTime, :endTime, :inRowSamples, :blobSamples, :aggregationLevel, :notValid, :dontAggregate);
+>>
+
+getSamplesBySourceIdsAndMetricIds(sourceIds, metricIds) ::= <<
+  select
+    source_id
+  , metric_id
+  , record_id
+  , sample_count
+  , in_row_samples
+  , blob_samples
+  , start_time
+  , end_time
+  , aggregation_level
+  , not_valid
+  , dont_aggregate
+  from timeline_chunks
+  where end_time >= :startTime
+  and start_time \<= :endTime
+  and source_id in (<sourceIds>)
+  <if(metricIds)>
+    and metric_id in (<metricIds>)
+  <endif>
+  and not_valid = 0
+  order by source_id, metric_id, start_time asc
+  ;
+>>
+
+insertLastStartTimes() ::= <<
+  insert into last_start_times (time_inserted, start_times)
+                        values (:timeInserted, :startTimes)
+>>
+
+getLastStartTimes() ::= <<
+  select time_inserted, start_times
+  from last_start_times
+  order by time_inserted desc
+  limit 1
+>>
+
+deleteLastStartTimes() ::= <<
+  delete from last_start_times
+>>
+
+bulkInsertSources() ::= <<
+  insert into sources (source_name, created_dt)
+  values (:sourceName, unix_timestamp());
+>>
+
+bulkInsertEventCategories() ::= <<
+  insert into event_categories (event_category)
+  values (:eventCategory);
+>>
+
+bulkInsertMetrics() ::= <<
+  insert into metrics (event_category_id, metric)
+  values (:eventCategoryId, :metric);
+>>
+
+bulkInsertTimelineChunks() ::= <<
+  insert into timeline_chunks (record_id, source_id, metric_id, sample_count, start_time, end_time, not_valid, dont_aggregate, aggregation_level, in_row_samples, blob_samples)
+  values (:chunkId, :sourceId, :metricId, :sampleCount, :startTime, :endTime, :dontAggregate, :notValid, :aggregationLevel, :inRowSamples, :blobSamples);
+>>
+
+getHighestTimelineChunkId() ::= <<
+  select record_id from timeline_chunks order by record_id desc limit 1;
+>>
+
+test() ::= <<
+  select 1;
+>>
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java b/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java
new file mode 100644
index 0000000..4d455b1
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/categories/TestCategoryAndMetrics.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.categories;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestCategoryAndMetrics extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testMapping() throws Exception {
+        final CategoryAndMetrics kinds = new CategoryAndMetrics("JVM");
+        kinds.addMetric("GC");
+        kinds.addMetric("CPU");
+
+        final ObjectMapper mapper = new ObjectMapper();
+        final String json = mapper.writeValueAsString(kinds);
+        Assert.assertEquals("{\"eventCategory\":\"JVM\",\"metrics\":[\"GC\",\"CPU\"]}", json);
+
+        final CategoryAndMetrics kindsFromJson = mapper.readValue(json, CategoryAndMetrics.class);
+        Assert.assertEquals(kindsFromJson, kinds);
+    }
+
+    @Test(groups = "fast")
+    public void testComparison() throws Exception {
+        final CategoryAndMetrics aKinds = new CategoryAndMetrics("JVM");
+        aKinds.addMetric("GC");
+        aKinds.addMetric("CPU");
+        Assert.assertEquals(aKinds.compareTo(aKinds), 0);
+
+        final CategoryAndMetrics bKinds = new CategoryAndMetrics("JVM");
+        bKinds.addMetric("GC");
+        bKinds.addMetric("CPU");
+        Assert.assertEquals(aKinds.compareTo(bKinds), 0);
+        Assert.assertEquals(bKinds.compareTo(aKinds), 0);
+
+        final CategoryAndMetrics cKinds = new CategoryAndMetrics("JVM");
+        cKinds.addMetric("GC");
+        cKinds.addMetric("CPU");
+        cKinds.addMetric("Something else");
+        Assert.assertTrue(aKinds.compareTo(cKinds) < 0);
+        Assert.assertTrue(cKinds.compareTo(aKinds) > 0);
+
+        final CategoryAndMetrics dKinds = new CategoryAndMetrics("ZVM");
+        dKinds.addMetric("GC");
+        Assert.assertTrue(aKinds.compareTo(dKinds) < 0);
+        Assert.assertTrue(dKinds.compareTo(aKinds) > 0);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java
new file mode 100644
index 0000000..a6620c5
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimeBytesAndSampleBytes.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestTimeBytesAndSampleBytes extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+        final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+        final TimeBytesAndSampleBytes timeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+
+        Assert.assertEquals(timeBytesAndSampleBytes.getTimeBytes(), timeBytes);
+        Assert.assertEquals(timeBytesAndSampleBytes.getSampleBytes(), sampleBytes);
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+        final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+
+        final TimeBytesAndSampleBytes timeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+        Assert.assertEquals(timeBytesAndSampleBytes, timeBytesAndSampleBytes);
+
+        final TimeBytesAndSampleBytes sameTimeBytesAndSampleBytes = new TimeBytesAndSampleBytes(timeBytes, sampleBytes);
+        Assert.assertEquals(sameTimeBytesAndSampleBytes, timeBytesAndSampleBytes);
+
+        final TimeBytesAndSampleBytes otherTimeBytesAndSampleBytes = new TimeBytesAndSampleBytes(sampleBytes, timeBytes);
+        Assert.assertNotEquals(otherTimeBytesAndSampleBytes, timeBytesAndSampleBytes);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java
new file mode 100644
index 0000000..e8c73f2
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/chunks/TestTimelineChunk.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.chunks;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestTimelineChunk extends UsageTestSuite {
+
+    private final Clock clock = new ClockMock();
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final long chunkId = 0L;
+        final int sourceId = 1;
+        final int metricId = 2;
+        final DateTime startTime = clock.getUTCNow();
+        final DateTime endTime = startTime.plusDays(2);
+        final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+        final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+        final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+
+        Assert.assertEquals(timelineChunk.getChunkId(), chunkId);
+        Assert.assertEquals(timelineChunk.getSourceId(), sourceId);
+        Assert.assertEquals(timelineChunk.getMetricId(), metricId);
+        Assert.assertEquals(timelineChunk.getStartTime(), startTime);
+        Assert.assertEquals(timelineChunk.getEndTime(), endTime);
+        Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getTimeBytes(), timeBytes);
+        Assert.assertEquals(timelineChunk.getTimeBytesAndSampleBytes().getSampleBytes(), sampleBytes);
+        Assert.assertEquals(timelineChunk.getAggregationLevel(), 0);
+        Assert.assertFalse(timelineChunk.getNotValid());
+        Assert.assertFalse(timelineChunk.getDontAggregate());
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final long chunkId = 0L;
+        final int sourceId = 1;
+        final int metricId = 2;
+        final DateTime startTime = clock.getUTCNow();
+        final DateTime endTime = startTime.plusDays(2);
+        final byte[] timeBytes = new byte[]{0x1, 0x2, 0x3};
+        final byte[] sampleBytes = new byte[]{0xA, 0xB, 0xC};
+
+        final TimelineChunk timelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+        Assert.assertEquals(timelineChunk, timelineChunk);
+
+        final TimelineChunk sameTimelineChunk = new TimelineChunk(chunkId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+        Assert.assertEquals(sameTimelineChunk, timelineChunk);
+
+        final TimelineChunk otherTimelineChunk = new TimelineChunk(sourceId, sourceId, metricId, startTime, endTime, timeBytes, sampleBytes, timeBytes.length);
+        Assert.assertNotEquals(otherTimelineChunk, timelineChunk);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java
new file mode 100644
index 0000000..4ab2631
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestEncodedBytesAndSampleCount.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestEncodedBytesAndSampleCount extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final byte[] encodedBytes = {0xA, 0xB};
+        final int sampleCount = 20;
+        final EncodedBytesAndSampleCount encodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+
+        Assert.assertEquals(encodedBytesAndSampleCount.getEncodedBytes(), encodedBytes);
+        Assert.assertEquals(encodedBytesAndSampleCount.getSampleCount(), sampleCount);
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final byte[] encodedBytes = {0xA, 0xB};
+        final int sampleCount = 20;
+
+        final EncodedBytesAndSampleCount encodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+        Assert.assertEquals(encodedBytesAndSampleCount, encodedBytesAndSampleCount);
+
+        final EncodedBytesAndSampleCount sameEncodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount);
+        Assert.assertEquals(sameEncodedBytesAndSampleCount, encodedBytesAndSampleCount);
+
+        final EncodedBytesAndSampleCount otherEncodedBytesAndSampleCount = new EncodedBytesAndSampleCount(encodedBytes, sampleCount + 1);
+        Assert.assertNotEquals(otherEncodedBytesAndSampleCount, encodedBytesAndSampleCount);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java
new file mode 100644
index 0000000..65529a9
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCoder.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.samples.RepeatSample;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCursor;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestSampleCoder extends UsageTestSuite {
+
+    private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+    private static final DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);
+    private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+    @Test(groups = "fast")
+    public void testScan() throws Exception {
+        final DateTime startTime = new DateTime(DateTimeZone.UTC);
+        final DateTime endTime = startTime.plusSeconds(5);
+        final List<DateTime> dateTimes = ImmutableList.<DateTime>of(startTime.plusSeconds(1), startTime.plusSeconds(2), startTime.plusSeconds(3), startTime.plusSeconds(4));
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+
+
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
+        final ScalarSample<Short> sample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 4);
+        sampleCoder.encodeSample(dataOutputStream, sample);
+        sampleCoder.encodeSample(dataOutputStream, new RepeatSample<Short>(3, sample));
+        dataOutputStream.close();
+
+        sampleCoder.scan(outputStream.toByteArray(), compressedTimes, dateTimes.size(), new TimeRangeSampleProcessor(startTime, endTime) {
+            @Override
+            public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+                Assert.assertTrue(time.isAfter(startTime));
+                Assert.assertTrue(time.isBefore(endTime));
+                Assert.assertEquals(Short.valueOf(value.toString()), sample.getSampleValue());
+            }
+        });
+    }
+
+    @Test(groups = "fast")
+    public void testTimeRangeSampleProcessor() throws Exception {
+        final DateTime startTime = new DateTime(dateFormatter.parseDateTime("2012-03-23T17:35:11.000Z"));
+        final DateTime endTime = new DateTime(dateFormatter.parseDateTime("2012-03-23T17:35:17.000Z"));
+        final int sampleCount = 2;
+
+        final List<DateTime> dateTimes = ImmutableList.<DateTime>of(startTime, endTime);
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+        final TimelineCursor cursor = new DefaultTimelineCursor(compressedTimes, sampleCount);
+        Assert.assertEquals(cursor.getNextTime(), startTime);
+        Assert.assertEquals(cursor.getNextTime(), endTime);
+
+        // 2 x the value 12: REPEAT_BYTE, SHORT, 2, SHORT, 12 (2 bytes)
+        final byte[] samples = new byte[]{(byte) 0xff, 2, 2, 0, 12};
+
+        final AtomicInteger samplesCount = new AtomicInteger(0);
+        sampleCoder.scan(samples, compressedTimes, sampleCount, new TimeRangeSampleProcessor(startTime, endTime) {
+            @Override
+            public void processOneSample(final DateTime time, final SampleOpcode opcode, final Object value) {
+                if (samplesCount.get() == 0) {
+                    Assert.assertEquals(DateTimeUtils.unixSeconds(time), DateTimeUtils.unixSeconds(startTime));
+                } else {
+                    Assert.assertEquals(DateTimeUtils.unixSeconds(time), DateTimeUtils.unixSeconds(endTime));
+                }
+                samplesCount.incrementAndGet();
+            }
+        });
+        Assert.assertEquals(samplesCount.get(), sampleCount);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(groups = "fast")
+    public void testCombineSampleBytes() throws Exception {
+        final ScalarSample[] samplesToChoose = new ScalarSample[]{new ScalarSample(SampleOpcode.DOUBLE, 2.0),
+                                                                  new ScalarSample(SampleOpcode.DOUBLE, 1.0),
+                                                                  new ScalarSample(SampleOpcode.INT_ZERO, 0)};
+        final int[] repetitions = new int[]{1, 2, 3, 4, 5, 240, 250, 300};
+        final Random rand = new Random(0);
+        int count = 0;
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(0, 0, sampleCoder);
+        final List<ScalarSample> samples = new ArrayList<ScalarSample>();
+        for (int i = 0; i < 20; i++) {
+            final ScalarSample sample = samplesToChoose[rand.nextInt(samplesToChoose.length)];
+            final int repetition = repetitions[rand.nextInt(repetitions.length)];
+            for (int r = 0; r < repetition; r++) {
+                samples.add(sample);
+                accum.addSample(sample);
+                count++;
+            }
+        }
+        final byte[] sampleBytes = sampleCoder.compressSamples(samples);
+        final byte[] accumBytes = accum.getEncodedSamples().getEncodedBytes();
+        Assert.assertEquals(accumBytes, sampleBytes);
+        final List<ScalarSample> restoredSamples = sampleCoder.decompressSamples(sampleBytes);
+        Assert.assertEquals(restoredSamples.size(), samples.size());
+        for (int i = 0; i < count; i++) {
+            Assert.assertEquals(restoredSamples.get(i), samples.get(i));
+        }
+        for (int fragmentLength = 2; fragmentLength < count / 2; fragmentLength++) {
+            final List<byte[]> fragments = new ArrayList<byte[]>();
+            final int fragmentCount = (int) Math.ceil((double) count / (double) fragmentLength);
+            for (int fragCounter = 0; fragCounter < fragmentCount; fragCounter++) {
+                final int fragIndex = fragCounter * fragmentLength;
+                final List<ScalarSample> fragment = samples.subList(fragIndex, Math.min(count, fragIndex + fragmentLength));
+                fragments.add(sampleCoder.compressSamples(fragment));
+            }
+            final byte[] combined = sampleCoder.combineSampleBytes(fragments);
+            final List<ScalarSample> restored = sampleCoder.decompressSamples(combined);
+            Assert.assertEquals(restored.size(), samples.size());
+            for (int i = 0; i < count; i++) {
+                Assert.assertEquals(restored.get(i), samples.get(i));
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(groups = "fast")
+    public void testCombineMoreThan65KSamples() throws Exception {
+        final int count = 0;
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(0, 0, sampleCoder);
+        final List<ScalarSample> samples = new ArrayList<ScalarSample>();
+        final ScalarSample sample1 = new ScalarSample(SampleOpcode.BYTE, (byte) 1);
+        final ScalarSample sample2 = new ScalarSample(SampleOpcode.BYTE, (byte) 2);
+        for (int i = 0; i < 20; i++) {
+            samples.add(sample1);
+            accum.addSample(sample1);
+        }
+        for (int i = 0; i < 0xFFFF + 100; i++) {
+            samples.add(sample2);
+            accum.addSample(sample2);
+        }
+        final byte[] sampleBytes = sampleCoder.compressSamples(samples);
+        final String hex = new String(Hex.encodeHex(sampleBytes));
+        // Here are the compressed samples: ff140101feffff0102ff640102
+        // Translation:
+        // [ff 14 01 01] means repeat 20 times BYTE value 1
+        // [fe ff ff 01 02] means repeat 65525 times BYTE value 2
+        // [ff 64 01 02] means repeat 100 times BYTE value 2
+        Assert.assertEquals(sampleBytes, Hex.decodeHex("ff140101feffff0102ff640102".toCharArray()));
+        final List<ScalarSample> restoredSamples = sampleCoder.decompressSamples(sampleBytes);
+        Assert.assertEquals(restoredSamples.size(), samples.size());
+        for (int i = 0; i < count; i++) {
+            Assert.assertEquals(restoredSamples.get(i), samples.get(i));
+        }
+    }
+
+    /*
+     * I saw an error in combineSampleBytes:
+     * java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Short
+     * These were the inputs:
+     * [11, 44, 74, -1, 2, 15, 11, 40, 68, -1, 2, 15]
+     * meaning half-float-for-double; repeat 2 times double zero; half-float-for-double; repeat 2 time double zero
+     * [11, 44, 68, -1, 3, 15, 11, 40, 68]
+     * meaning meaning half-float-for-double; repeat 3 times double zero; half-float-for-double
+     * [-1, 3, 15, 11, 40, 68, -1, 2, 15, 11, 40, 68]
+     * meaning repeat 3 times double-zero; half-float-for-double; repeat 2 times double zero; half-float-for-double
+     * [-1, 2, 11, 40, 68, -1, 3, 15, 11, 40, 68, 15]
+     * meaning repeat 2 times half-float-for-double; repeat 3 times double-zero; half-float-for-double; double zero
+     */
+    @SuppressWarnings("unchecked")
+    @Test(groups = "fast")
+    public void testCombineError() throws Exception {
+        final byte[] b1 = new byte[]{11, 44, 74, -1, 2, 15, 11, 40, 68, -1, 2, 15};
+        final byte[] b2 = new byte[]{11, 44, 68, -1, 3, 15, 11, 40, 68};
+        final byte[] b3 = new byte[]{-1, 3, 15, 11, 40, 68, -1, 2, 15, 11, 40, 68};
+        final byte[] b4 = new byte[]{-1, 2, 11, 40, 68, -1, 3, 15, 11, 40, 68, 15};
+        final List<byte[]> parts = new ArrayList<byte[]>();
+        parts.add(b1);
+        parts.add(b2);
+        parts.add(b3);
+        parts.add(b4);
+        final byte[] combinedBytes = sampleCoder.combineSampleBytes(parts);
+        final List<ScalarSample> samples = sampleCoder.decompressSamples(combinedBytes);
+        Assert.assertEquals(samples.size(), 25);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java
new file mode 100644
index 0000000..d572661
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestSampleCompression.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+public class TestSampleCompression extends UsageTestSuite {
+
+    private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+    private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+    @Test(groups = "fast")
+    public void testBasicDoubleCompression() throws Exception {
+
+        checkDoubleCodedResult(0.0, SampleOpcode.DOUBLE_ZERO, 1);
+        checkDoubleCodedResult(1.0, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+        checkDoubleCodedResult(1.005, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+        checkDoubleCodedResult(127.2, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+        checkDoubleCodedResult(-128.2, SampleOpcode.BYTE_FOR_DOUBLE, 2);
+
+        checkDoubleCodedResult(65503.0, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(-65503.0, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(6.1e-5, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(-6.1e-5, SampleOpcode.HALF_FLOAT_FOR_DOUBLE, 3);
+
+        checkDoubleCodedResult(200.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(32767.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(-200.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+        checkDoubleCodedResult(-32768.0, SampleOpcode.SHORT_FOR_DOUBLE, 3);
+
+        checkDoubleCodedResult((double) Float.MAX_VALUE, SampleOpcode.FLOAT_FOR_DOUBLE, 5);
+        checkDoubleCodedResult((double) Float.MIN_VALUE, SampleOpcode.FLOAT_FOR_DOUBLE, 5);
+
+        checkDoubleCodedResult(((double) Float.MAX_VALUE) * 10.0, SampleOpcode.DOUBLE, 9);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void checkDoubleCodedResult(final double value, final SampleOpcode expectedOpcode, final int expectedSize) {
+        final ScalarSample codedSample = sampleCoder.compressSample(new ScalarSample(SampleOpcode.DOUBLE, value));
+        Assert.assertEquals(codedSample.getOpcode(), expectedOpcode);
+        final double error = value == 0.0 ? 0.0 : Math.abs((value - codedSample.getDoubleValue()) / value);
+        Assert.assertTrue(error <= sampleCoder.getMaxFractionError());
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(123, 456, sampleCoder);
+        accum.addSample(codedSample);
+        final DateTime now = new DateTime();
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        dateTimes.add(now);
+        final byte[] timeBytes = timelineCoder.compressDateTimes(dateTimes);
+        final byte[] encodedSampleBytes = accum.extractTimelineChunkAndReset(now, now, timeBytes).getTimeBytesAndSampleBytes().getSampleBytes();
+        Assert.assertEquals(encodedSampleBytes.length, expectedSize);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java
new file mode 100644
index 0000000..1afa1ae
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkAccumulator.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCursor;
+
+public class TestTimelineChunkAccumulator extends UsageTestSuite {
+
+    private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+    private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+    @SuppressWarnings("unchecked")
+    @Test(groups = "fast")
+    public void testBasicAccumulator() throws Exception {
+        final int hostId = 123;
+        final int sampleKindId = 456;
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        final DateTime startTime = new DateTime();
+        final DateTime endTime = startTime.plus(1000);
+
+        accum.addSample(new ScalarSample(SampleOpcode.INT, 25));
+        int timesCounter = 0;
+        dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+        for (int i = 0; i < 5; i++) {
+            accum.addSample(new ScalarSample(SampleOpcode.INT, 10));
+            dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+        }
+        accum.addSample(new ScalarSample(SampleOpcode.DOUBLE, 100.0));
+        dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+        accum.addSample(new ScalarSample(SampleOpcode.DOUBLE, 100.0));
+        dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+
+        accum.addSample(new ScalarSample(SampleOpcode.STRING, "Hiya!"));
+        dateTimes.add(startTime.plusSeconds(30 * timesCounter++));
+
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+        final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+        Assert.assertEquals(chunk.getSampleCount(), 9);
+        // Now play them back
+        sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+            private int sampleNumber = 0;
+
+            @Override
+            public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+                if (sampleNumber == 0) {
+                    Assert.assertEquals(opcode, SampleOpcode.INT);
+                    Assert.assertEquals(value, 25);
+                } else if (sampleNumber >= 1 && sampleNumber < 6) {
+                    Assert.assertEquals(opcode, SampleOpcode.INT);
+                    Assert.assertEquals(value, 10);
+                } else if (sampleNumber >= 6 && sampleNumber < 8) {
+                    Assert.assertEquals(opcode, SampleOpcode.DOUBLE);
+                    Assert.assertEquals(value, 100.0);
+                } else if (sampleNumber == 8) {
+                    Assert.assertEquals(opcode, SampleOpcode.STRING);
+                    Assert.assertEquals(value, "Hiya!");
+                } else {
+                    Assert.assertTrue(false);
+                }
+                sampleNumber += sampleCount;
+            }
+        });
+        final TimelineChunkDecoded chunkDecoded = new TimelineChunkDecoded(chunk, sampleCoder);
+        //System.out.printf("%s\n", chunkDecoded.toString());
+    }
+
+
+    @Test(groups = "fast")
+    public void testByteRepeater() throws Exception {
+        final int hostId = 123;
+        final int sampleKindId = 456;
+        final DateTime startTime = new DateTime();
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        final int byteRepeaterCount = 255;
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+        for (int i = 0; i < byteRepeaterCount; i++) {
+            dateTimes.add(startTime.plusSeconds(i * 5));
+            accum.addSample(sampleCoder.compressSample(new ScalarSample<Double>(SampleOpcode.DOUBLE, 2.0)));
+        }
+        final DateTime endTime = startTime.plusSeconds(5 * byteRepeaterCount);
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+        final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+        final byte[] samples = chunk.getTimeBytesAndSampleBytes().getSampleBytes();
+        // Should be 0xFF 0xFF 0x12 0x02
+        Assert.assertEquals(samples.length, 4);
+        Assert.assertEquals(((int) samples[0]) & 0xff, SampleOpcode.REPEAT_BYTE.getOpcodeIndex());
+        Assert.assertEquals(((int) samples[1]) & 0xff, byteRepeaterCount);
+        Assert.assertEquals(((int) samples[2]) & 0xff, SampleOpcode.BYTE_FOR_DOUBLE.getOpcodeIndex());
+        Assert.assertEquals(((int) samples[3]) & 0xff, 0x02);
+        Assert.assertEquals(chunk.getSampleCount(), byteRepeaterCount);
+        sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+
+            @Override
+            public void processSamples(final TimelineCursor timeCursor, final int sampleCount, final SampleOpcode opcode, final Object value) {
+                Assert.assertEquals(sampleCount, byteRepeaterCount);
+                Assert.assertEquals(value, 2.0);
+            }
+        });
+    }
+
+    @Test(groups = "fast")
+    public void testShortRepeater() throws Exception {
+        final int hostId = 123;
+        final int sampleKindId = 456;
+        final DateTime startTime = new DateTime();
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        final int shortRepeaterCount = 256;
+        final TimelineChunkAccumulator accum = new TimelineChunkAccumulator(hostId, sampleKindId, sampleCoder);
+        for (int i = 0; i < shortRepeaterCount; i++) {
+            dateTimes.add(startTime.plusSeconds(i * 5));
+            accum.addSample(sampleCoder.compressSample(new ScalarSample<Double>(SampleOpcode.DOUBLE, 2.0)));
+        }
+        final DateTime endTime = startTime.plusSeconds(5 * shortRepeaterCount);
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(dateTimes);
+        final TimelineChunk chunk = accum.extractTimelineChunkAndReset(startTime, endTime, compressedTimes);
+        final byte[] samples = chunk.getTimeBytesAndSampleBytes().getSampleBytes();
+        Assert.assertEquals(samples.length, 5);
+        Assert.assertEquals(((int) samples[0]) & 0xff, SampleOpcode.REPEAT_SHORT.getOpcodeIndex());
+        final int count = ((samples[1] & 0xff) << 8) | (samples[2] & 0xff);
+        Assert.assertEquals(count, shortRepeaterCount);
+        Assert.assertEquals(((int) samples[3]) & 0xff, SampleOpcode.BYTE_FOR_DOUBLE.getOpcodeIndex());
+        Assert.assertEquals(((int) samples[4]) & 0xff, 0x02);
+        Assert.assertEquals(chunk.getSampleCount(), shortRepeaterCount);
+
+        sampleCoder.scan(chunk.getTimeBytesAndSampleBytes().getSampleBytes(), compressedTimes, dateTimes.size(), new SampleProcessor() {
+
+            @Override
+            public void processSamples(TimelineCursor timeCursor, int sampleCount, SampleOpcode opcode, Object value) {
+                Assert.assertEquals(sampleCount, shortRepeaterCount);
+                Assert.assertEquals(value, 2.0);
+            }
+        });
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java
new file mode 100644
index 0000000..9cc56d2
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/codec/TestTimelineChunkToJson.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.codec;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.chunks.TimelineChunk;
+import com.ning.billing.usage.timeline.chunks.TimelineChunksViews.Compact;
+import com.ning.billing.usage.timeline.chunks.TimelineChunksViews.Loose;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+import com.ning.billing.usage.timeline.samples.ScalarSample;
+import com.ning.billing.usage.timeline.times.DefaultTimelineCoder;
+import com.ning.billing.usage.timeline.times.TimelineCoder;
+
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+public class TestTimelineChunkToJson extends UsageTestSuite {
+
+    private static final ObjectMapper mapper = new ObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
+    private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+    private static final SampleCoder sampleCoder = new DefaultSampleCoder();
+
+    private static final long CHUNK_ID = 1242L;
+    private static final int HOST_ID = 1422;
+    private static final int SAMPLE_KIND_ID = 1224;
+    private static final int SAMPLE_COUNT = 2142;
+    private static final DateTime END_TIME = new DateTime(DateTimeZone.UTC);
+    private static final DateTime START_TIME = END_TIME.minusMinutes(SAMPLE_COUNT);
+
+    private byte[] timeBytes;
+    private byte[] samples;
+    private TimelineChunk chunk;
+
+    @BeforeMethod(groups = "fast")
+    public void setUp() throws Exception {
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        final DataOutputStream output = new DataOutputStream(out);
+        for (int i = 0; i < SAMPLE_COUNT; i++) {
+            sampleCoder.encodeSample(output, new ScalarSample<Long>(SampleOpcode.LONG, 10L));
+            dateTimes.add(START_TIME.plusMinutes(i));
+        }
+        output.flush();
+        output.close();
+        samples = out.toByteArray();
+
+        final DateTime endTime = dateTimes.get(dateTimes.size() - 1);
+        timeBytes = timelineCoder.compressDateTimes(dateTimes);
+        chunk = new TimelineChunk(CHUNK_ID, HOST_ID, SAMPLE_KIND_ID, START_TIME, endTime, timeBytes, samples, SAMPLE_COUNT);
+    }
+
+    @Test(groups = "fast")
+    public void testTimelineChunkCompactMapping() throws Exception {
+        final String chunkToString = mapper.writerWithView(Compact.class).writeValueAsString(chunk);
+        final Map chunkFromString = mapper.readValue(chunkToString, Map.class);
+        Assert.assertEquals(chunkFromString.keySet().size(), 10);
+        Assert.assertEquals(chunkFromString.get("sourceId"), HOST_ID);
+        Assert.assertEquals(chunkFromString.get("metricId"), SAMPLE_KIND_ID);
+        final Map<String, String> timeBytesAndSampleBytes = (Map<String, String>) chunkFromString.get("timeBytesAndSampleBytes");
+        Assert.assertEquals(new TextNode(timeBytesAndSampleBytes.get("timeBytes")).binaryValue(), timeBytes);
+        Assert.assertEquals(new TextNode(timeBytesAndSampleBytes.get("sampleBytes")).binaryValue(), samples);
+        Assert.assertEquals(chunkFromString.get("sampleCount"), SAMPLE_COUNT);
+        Assert.assertEquals(chunkFromString.get("aggregationLevel"), 0);
+        Assert.assertEquals(chunkFromString.get("notValid"), false);
+        Assert.assertEquals(chunkFromString.get("dontAggregate"), false);
+        Assert.assertEquals(chunkFromString.get("chunkId"), (int) CHUNK_ID);
+    }
+
+    @Test(groups = "fast")
+    public void testTimelineChunkLooseMapping() throws Exception {
+        final String chunkToString = mapper.writerWithView(Loose.class).writeValueAsString(chunk);
+        final Map chunkFromString = mapper.readValue(chunkToString, Map.class);
+        Assert.assertEquals(chunkFromString.keySet().size(), 3);
+        Assert.assertEquals(chunkFromString.get("sourceId"), HOST_ID);
+        Assert.assertEquals(chunkFromString.get("metricId"), SAMPLE_KIND_ID);
+        Assert.assertEquals(chunkFromString.get("chunkId"), (int) CHUNK_ID);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java b/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java
new file mode 100644
index 0000000..98fb6eb
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/filter/TestDecimatingFilter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.filter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.joda.time.DateTime;
+import org.skife.config.TimeSpan;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.consumer.SampleConsumer;
+import com.ning.billing.usage.timeline.samples.SampleOpcode;
+
+public class TestDecimatingFilter extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testBasicFilterOperations() throws Exception {
+        final List<Double> outputs = new ArrayList<Double>();
+        final long millisStart = System.currentTimeMillis() - 2000 * 100;
+
+        final DecimatingSampleFilter filter = new DecimatingSampleFilter(new DateTime(millisStart), new DateTime(millisStart + 2000 * 100), 25, 100, new TimeSpan("2s"), DecimationMode.PEAK_PICK,
+                                                                         new SampleConsumer() {
+
+                                                                             @Override
+                                                                             public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+                                                                                 outputs.add((double) ((Double) value));
+                                                                             }
+                                                                         });
+        for (int i = 0; i < 100; i++) {
+            // Make the value go up for 4 samples; then down for 4 samples, between 10.0 and 40.0
+            final int index = (i % 8) + 1;
+            double value = 0;
+            if (index <= 4) {
+                value = 10.0 * index;
+            } else {
+                value = (8 - (index - 1)) * 10;
+            }
+            //System.out.printf("For i %d, index %d, adding value %f\n", i, index, value);
+            filter.processOneSample(new DateTime(millisStart + 2000 * i), SampleOpcode.DOUBLE, value);
+        }
+        int index = 0;
+        for (final Double value : outputs) {
+            //System.out.printf("index %d, value %f\n", index++, (double)((Double)value));
+            if ((index & 1) == 0) {
+                Assert.assertEquals(value, 40.0);
+            } else {
+                Assert.assertEquals(value, 10.0);
+            }
+            index++;
+        }
+    }
+
+    /**
+     * This test has sample count of 21, and output count of 6, so there are 5.8 samples per output point
+     *
+     * @throws Exception
+     */
+    @Test(groups = "fast")
+    public void testFilterWithNonAlignedSampleCounts() throws Exception {
+        final List<Double> outputs = new ArrayList<Double>();
+        final long millisStart = System.currentTimeMillis() - 2000 * 21;
+
+        final DecimatingSampleFilter filter = new DecimatingSampleFilter(new DateTime(millisStart), new DateTime(millisStart + 2000 * 21), 6, 21, new TimeSpan("2s"), DecimationMode.PEAK_PICK,
+                                                                         new SampleConsumer() {
+
+                                                                             @Override
+                                                                             public void consumeSample(final int sampleNumber, final SampleOpcode opcode, final Object value, final DateTime time) {
+                                                                                 outputs.add((double) ((Double) value));
+                                                                             }
+                                                                         });
+        for (int i = 0; i < 21; i++) {
+            // Make the value go up for 6 samples; then down for 6 samples, between 10.0 and 60.0
+            final int index = (i % 6) + 1;
+            double value = 0;
+            if (index <= 3) {
+                value = 10.0 * index;
+            } else {
+                value = (6 - (index - 1)) * 10;
+            }
+            //System.out.printf("For i %d, index %d, adding value %f\n", i, index, value);
+            filter.processOneSample(new DateTime(millisStart + 2000 * i), SampleOpcode.DOUBLE, value);
+        }
+        Assert.assertEquals(outputs.size(), 5);
+        final double[] expectedValues = new double[]{30.0, 20.0, 30.0, 30.0, 10.0};
+        for (int i = 0; i < 5; i++) {
+            final double value = outputs.get(i);
+            final double expectedValue = expectedValues[i];
+            //System.out.printf("index %d, value returned %f, value expected %f\n", i, value, expectedValue);
+            Assert.assertEquals(value, expectedValue);
+        }
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java b/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java
new file mode 100644
index 0000000..9bd9d86
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/metrics/TestSamplesForMetricAndSource.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.metrics;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestSamplesForMetricAndSource extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testMapping() throws Exception {
+        final SamplesForMetricAndSource samples = new SamplesForMetricAndSource("host.foo.com", "JVM", "GC", "1,2,2,0");
+
+        final ObjectMapper mapper = new ObjectMapper();
+        final String json = mapper.writeValueAsString(samples);
+        Assert.assertEquals("{\"sourceName\":\"host.foo.com\",\"eventCategory\":\"JVM\",\"metric\":\"GC\",\"samples\":\"1,2,2,0\"}", json);
+
+        final SamplesForMetricAndSource samplesFromJson = mapper.readValue(json, SamplesForMetricAndSource.class);
+        Assert.assertEquals(samplesFromJson, samples);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java b/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java
new file mode 100644
index 0000000..93f7925
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/persistent/TestReplayer.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.persistent;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.sources.SourceSamplesForTimestamp;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+public class TestReplayer extends UsageTestSuite {
+
+    private static final File basePath = new File(System.getProperty("java.io.tmpdir"), "TestReplayer-" + System.currentTimeMillis());
+
+    private static final class MockReplayer extends Replayer {
+
+        private final List<File> expectedFiles;
+        private int seen = 0;
+
+        public MockReplayer(final String path, final List<File> expectedFiles) {
+            super(path);
+            this.expectedFiles = expectedFiles;
+        }
+
+        @Override
+        public void read(final File file, final Function<SourceSamplesForTimestamp, Void> fn) throws IOException {
+            Assert.assertEquals(file, expectedFiles.get(seen));
+            seen++;
+        }
+
+        public int getSeen() {
+            return seen;
+        }
+    }
+
+    private final StreamyBytesPersistentOutputStream outputStream = new StreamyBytesPersistentOutputStream(basePath.toString(), "pweet", null, true);
+
+    @Test(groups = "fast")
+    public void testStringOrdering() throws Exception {
+        final File file1 = new File("aaa.bbb.12345.bin");
+        final File file2 = new File("aaa.bbb.12346.bin");
+        final File file3 = new File("aaa.bbb.02345.bin");
+
+        final List<File> sortedCopy = Replayer.FILE_ORDERING.sortedCopy(ImmutableList.<File>of(file2, file1, file3));
+        Assert.assertEquals(sortedCopy.get(0), file3);
+        Assert.assertEquals(sortedCopy.get(1), file1);
+        Assert.assertEquals(sortedCopy.get(2), file2);
+    }
+
+    @Test(groups = "slow")
+    public void testOrdering() throws Exception {
+        Assert.assertTrue(basePath.mkdir());
+
+        final List<String> filePathsCreated = new ArrayList<String>();
+        final List<File> filesCreated = new ArrayList<File>();
+        final int expected = 50;
+
+        for (int i = 0; i < expected; i++) {
+            filePathsCreated.add(outputStream.getFileName());
+            Thread.sleep(17);
+        }
+
+        // Create the files in the opposite ordering to make sure we can re-read them in order
+        for (int i = expected - 1; i >= 0; i--) {
+            final File file = new File(filePathsCreated.get(i));
+            Assert.assertTrue(file.createNewFile());
+            filesCreated.add(file);
+        }
+
+        final MockReplayer replayer = new MockReplayer(basePath.toString(), Lists.reverse(filesCreated));
+        replayer.readAll();
+
+        Assert.assertEquals(replayer.getSeen(), expected);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java
new file mode 100644
index 0000000..dc8f5ef
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestNullSample.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestNullSample extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testConstructor() throws Exception {
+        final NullSample sample = new NullSample();
+
+        Assert.assertEquals(sample.getOpcode(), SampleOpcode.NULL);
+        Assert.assertNull(sample.getSampleValue());
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java
new file mode 100644
index 0000000..aac97ec
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestRepeatSample.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestRepeatSample extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final int repeatCount = 5;
+        final ScalarSample<Short> scalarSample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 12);
+        final RepeatSample<Short> repeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+
+        Assert.assertEquals(repeatSample.getRepeatCount(), repeatCount);
+        Assert.assertEquals(repeatSample.getSampleRepeated(), scalarSample);
+        Assert.assertEquals(repeatSample.getOpcode().name(), SampleOpcode.REPEAT_BYTE.name());
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final int repeatCount = 5;
+        final ScalarSample<Short> scalarSample = new ScalarSample<Short>(SampleOpcode.SHORT, (short) 12);
+
+        final RepeatSample<Short> repeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+        Assert.assertEquals(repeatSample, repeatSample);
+
+        final RepeatSample<Short> sameRepeatSample = new RepeatSample<Short>(repeatCount, scalarSample);
+        Assert.assertEquals(sameRepeatSample, repeatSample);
+
+        final RepeatSample<Short> otherRepeatSample = new RepeatSample<Short>(repeatCount + 1, scalarSample);
+        Assert.assertNotEquals(otherRepeatSample, repeatSample);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java
new file mode 100644
index 0000000..3243c54
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestSampleOpcode.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestSampleOpcode extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetKnownOpcodeFromIndex() throws Exception {
+        for (final SampleOpcode opcode : SampleOpcode.values()) {
+            final SampleOpcode opcodeFromIndex = SampleOpcode.getOpcodeFromIndex(opcode.getOpcodeIndex());
+            Assert.assertEquals(opcodeFromIndex, opcode);
+
+            Assert.assertEquals(opcodeFromIndex.getOpcodeIndex(), opcode.getOpcodeIndex());
+            Assert.assertEquals(opcodeFromIndex.getByteSize(), opcode.getByteSize());
+            Assert.assertEquals(opcodeFromIndex.getNoArgs(), opcode.getNoArgs());
+            Assert.assertEquals(opcodeFromIndex.getRepeater(), opcode.getRepeater());
+            Assert.assertEquals(opcodeFromIndex.getReplacement(), opcode.getReplacement());
+        }
+    }
+
+    @Test(groups = "fast", expectedExceptions = IllegalArgumentException.class)
+    public void testgetUnknownOpcodeFromIndex() throws Exception {
+        SampleOpcode.getOpcodeFromIndex(Integer.MAX_VALUE);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java
new file mode 100644
index 0000000..0436ed4
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/samples/TestScalarSample.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.samples;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestScalarSample extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final SampleOpcode opcode = SampleOpcode.SHORT;
+        final short value = (short) 5;
+        final ScalarSample<Short> scalarSample = new ScalarSample<Short>(opcode, value);
+
+        Assert.assertEquals(scalarSample.getOpcode(), opcode);
+        Assert.assertEquals((short) scalarSample.getSampleValue(), value);
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final SampleOpcode opcode = SampleOpcode.SHORT;
+        final short value = (short) 5;
+
+        final ScalarSample<Short> scalarSample = new ScalarSample<Short>(opcode, value);
+        Assert.assertEquals(scalarSample, scalarSample);
+
+        final ScalarSample<Short> sameScalarSample = new ScalarSample<Short>(opcode, value);
+        Assert.assertEquals(sameScalarSample, scalarSample);
+
+        final ScalarSample<Short> otherScalarSample = new ScalarSample<Short>(opcode, (short) (value + 1));
+        Assert.assertNotEquals(otherScalarSample, scalarSample);
+    }
+
+    @Test(groups = "fast")
+    public void testFromObject() throws Exception {
+        verifyFromObject(null, 0.0, null, SampleOpcode.NULL);
+
+        verifyFromObject((byte) 1, (double) 1, (byte) 1, SampleOpcode.BYTE);
+
+        verifyFromObject((short) 128, (double) 128, (short) 128, SampleOpcode.SHORT);
+        verifyFromObject(32767, (double) 32767, (short) 32767, SampleOpcode.SHORT);
+
+        verifyFromObject(32768, (double) 32768, 32768, SampleOpcode.INT);
+        verifyFromObject((long) 32767, (double) 32767, (short) 32767, SampleOpcode.SHORT);
+        verifyFromObject((long) 32768, (double) 32768, 32768, SampleOpcode.INT);
+
+        verifyFromObject(2147483648L, (double) 2147483648L, 2147483648L, SampleOpcode.LONG);
+
+        verifyFromObject((float) 1, 1, (float) 1, SampleOpcode.FLOAT);
+
+        verifyFromObject(12.24, 12.24, 12.24, SampleOpcode.DOUBLE);
+    }
+
+    private void verifyFromObject(final Object value, final double expectedDoubleValue, final Object expectedSampleValue, final SampleOpcode expectedSampleOpcode) {
+        final ScalarSample scalarSample = ScalarSample.fromObject(value);
+        Assert.assertEquals(scalarSample.getOpcode(), expectedSampleOpcode);
+        Assert.assertEquals(scalarSample.getSampleValue(), expectedSampleValue);
+        Assert.assertEquals(scalarSample.getDoubleValue(), expectedDoubleValue);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java b/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java
new file mode 100644
index 0000000..d5c60c1
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/TestDateTimeUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline;
+
+import org.joda.time.DateTime;
+import org.joda.time.Seconds;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestDateTimeUtils extends UsageTestSuite {
+
+    private final Clock clock = new ClockMock();
+
+    @Test(groups = "fast")
+    public void testRoundTrip() throws Exception {
+        final DateTime utcNow = clock.getUTCNow();
+        final int unixSeconds = DateTimeUtils.unixSeconds(utcNow);
+        final DateTime dateTimeFromUnixSeconds = DateTimeUtils.dateTimeFromUnixSeconds(unixSeconds);
+
+        Assert.assertEquals(Seconds.secondsBetween(dateTimeFromUnixSeconds, utcNow).getSeconds(), 0);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java
new file mode 100644
index 0000000..97931e8
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestDefaultTimelineCoder.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+import com.ning.billing.usage.timeline.util.DateTimeUtils;
+import com.ning.billing.usage.timeline.util.Hex;
+
+public class TestDefaultTimelineCoder extends UsageTestSuite {
+
+    private static final TimelineCoder timelineCoder = new DefaultTimelineCoder();
+
+    @Test(groups = "fast")
+    public void testBasicEncodeDecode() throws Exception {
+        final DateTime firstTime = DateTimeUtils.dateTimeFromUnixSeconds(1000000);
+        final List<DateTime> unencodedTimes = makeSomeTimes(firstTime);
+
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(unencodedTimes);
+        //System.out.printf("Compressed times: %s\n", new String(Hex.encodeHex(compressedTimes)));
+        final List<DateTime> decompressedTimes = timelineCoder.decompressDateTimes(compressedTimes);
+        Assert.assertEquals(decompressedTimes.size(), unencodedTimes.size());
+        for (int i = 0; i < unencodedTimes.size(); i++) {
+            Assert.assertEquals(decompressedTimes.get(i), unencodedTimes.get(i));
+        }
+    }
+
+    private List<DateTime> makeSomeTimes(final DateTime firstTime) {
+        final List<DateTime> times = new ArrayList<DateTime>();
+        Collections.addAll(times, firstTime, firstTime.plusSeconds(5), firstTime.plusSeconds(5), firstTime.plusSeconds(5),
+                           firstTime.plusSeconds(1000), firstTime.plusSeconds(1000), firstTime.plusSeconds(2030), firstTime.plusSeconds(2060));
+        return times;
+    }
+
+    @Test(groups = "fast")
+    public void testRepeats() throws Exception {
+        final DateTime firstTime = DateTimeUtils.dateTimeFromUnixSeconds(1293846);
+        final List<DateTime> unencodedTimes = makeSomeTimes(firstTime);
+
+        final byte[] compressedTimes = timelineCoder.compressDateTimes(unencodedTimes);
+        final List<DateTime> decompressedTimes = timelineCoder.decompressDateTimes(compressedTimes);
+        Assert.assertEquals(decompressedTimes.size(), unencodedTimes.size());
+        for (int i = 0; i < unencodedTimes.size(); i++) {
+            Assert.assertEquals(decompressedTimes.get(i), unencodedTimes.get(i));
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testCombiningTimelinesByteRepeats() throws Exception {
+        final int firstTime = 1293846;
+
+        final List<DateTime> unencodedTimes1 = new ArrayList<DateTime>();
+        final List<DateTime> unencodedTimes2 = new ArrayList<DateTime>();
+        final int sampleCount = 10;
+        for (int i = 0; i < sampleCount; i++) {
+            unencodedTimes1.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + i * 100));
+            unencodedTimes2.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + sampleCount * 100 + i * 100));
+        }
+        final byte[] compressedTimes1 = timelineCoder.compressDateTimes(unencodedTimes1);
+        final byte[] compressedTimes2 = timelineCoder.compressDateTimes(unencodedTimes2);
+        Assert.assertEquals(compressedTimes1.length, 8);
+        Assert.assertEquals(compressedTimes1[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes1[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes1[6] & 0xff, 9);
+        Assert.assertEquals(compressedTimes1[7] & 0xff, 100);
+        Assert.assertEquals(compressedTimes2.length, 8);
+        Assert.assertEquals(compressedTimes2[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes2[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes2[6] & 0xff, 9);
+        Assert.assertEquals(compressedTimes2[7] & 0xff, 100);
+        final List<byte[]> timesList = new ArrayList<byte[]>();
+        timesList.add(compressedTimes1);
+        timesList.add(compressedTimes2);
+        final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+        Assert.assertEquals(combinedTimes.length, 8);
+        Assert.assertEquals(combinedTimes[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(combinedTimes[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+        Assert.assertEquals(combinedTimes[6] & 0xff, 19);
+        Assert.assertEquals(combinedTimes[7] & 0xff, 100);
+        // Check for 19, not 20, since the first full time took one
+        Assert.assertEquals(combinedTimes[6], 19);
+        Assert.assertEquals(timelineCoder.countTimeBytesSamples(combinedTimes), 20);
+    }
+
+    @Test(groups = "fast")
+    public void testCombiningTimelinesShortRepeats() throws Exception {
+        final int sampleCount = 240;
+        final int firstTime = 1293846;
+        final List<DateTime> unencodedTimes1 = new ArrayList<DateTime>();
+        final List<DateTime> unencodedTimes2 = new ArrayList<DateTime>();
+        for (int i = 0; i < sampleCount; i++) {
+            unencodedTimes1.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + i * 100));
+            unencodedTimes2.add(DateTimeUtils.dateTimeFromUnixSeconds(firstTime + sampleCount * 100 + i * 100));
+        }
+        final byte[] compressedTimes1 = timelineCoder.compressDateTimes(unencodedTimes1);
+        final byte[] compressedTimes2 = timelineCoder.compressDateTimes(unencodedTimes2);
+        Assert.assertEquals(compressedTimes1.length, 8);
+        Assert.assertEquals(compressedTimes1[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes1[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes1[6] & 0xff, sampleCount - 1);
+        Assert.assertEquals(compressedTimes1[7] & 0xff, 100);
+        Assert.assertEquals(compressedTimes2.length, 8);
+        Assert.assertEquals(compressedTimes2[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes2[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_BYTE.getOpcodeIndex());
+        Assert.assertEquals(compressedTimes2[6] & 0xff, sampleCount - 1);
+        Assert.assertEquals(compressedTimes2[7] & 0xff, 100);
+        final List<byte[]> timesList = new ArrayList<byte[]>();
+        timesList.add(compressedTimes1);
+        timesList.add(compressedTimes2);
+        final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+        Assert.assertEquals(combinedTimes.length, 9);
+        Assert.assertEquals(combinedTimes[0] & 0xff, TimelineOpcode.FULL_TIME.getOpcodeIndex());
+        Assert.assertEquals(combinedTimes[5] & 0xff, TimelineOpcode.REPEATED_DELTA_TIME_SHORT.getOpcodeIndex());
+        Assert.assertEquals(combinedTimes[6] & 0xff, 1);
+        Assert.assertEquals(combinedTimes[7] & 0xff, sampleCount * 2 - 1 - 256);
+        Assert.assertEquals(combinedTimes[8], 100);
+    }
+
+    @Test(groups = "fast")
+    public void testCombiningShortFragments() throws Exception {
+        final byte[] fragment0 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) 84, (byte) 20};
+        final byte[] fragment1 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -122, (byte) 30};
+        final byte[] fragment2 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -62, (byte) 30};
+        final byte[] fragment3 = new byte[]{(byte) -1, (byte) 0, (byte) 15, (byte) 66, (byte) -2, (byte) 30};
+        final byte[][] fragmentArray = new byte[][]{fragment0, fragment1, fragment2, fragment3};
+        final byte[] combined = timelineCoder.combineTimelines(Arrays.asList(fragmentArray), null);
+        final List<DateTime> restoredTimes = timelineCoder.decompressDateTimes(combined);
+        final List<List<DateTime>> fragmentIntTimes = new ArrayList<List<DateTime>>();
+        final List<DateTime> allFragmentTimes = new ArrayList<DateTime>();
+        int totalLength = 0;
+        for (final byte[] aFragmentArray : fragmentArray) {
+            final List<DateTime> fragmentTimes = timelineCoder.decompressDateTimes(aFragmentArray);
+            fragmentIntTimes.add(fragmentTimes);
+            totalLength += fragmentTimes.size();
+            for (final DateTime time : fragmentTimes) {
+                allFragmentTimes.add(time);
+            }
+        }
+        Assert.assertEquals(restoredTimes.size(), totalLength);
+        for (int i = 0; i < totalLength; i++) {
+            Assert.assertEquals(restoredTimes.get(i), allFragmentTimes.get(i));
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testCombiningTimelinesRandomRepeats() throws Exception {
+        final int[] increments = new int[]{30, 45, 10, 30, 20};
+        final int[] repetitions = new int[]{1, 2, 3, 4, 5, 240, 250, 300};
+        final int firstTimeInt = 1000000;
+        final DateTime startTime = DateTimeUtils.dateTimeFromUnixSeconds(firstTimeInt);
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        final Random rand = new Random(0);
+        DateTime nextTime = startTime;
+        int count = 0;
+        for (int i = 0; i < 20; i++) {
+            final int increment = increments[rand.nextInt(increments.length)];
+            final int repetition = repetitions[rand.nextInt(repetitions.length)];
+            for (int r = 0; i < repetition; i++) {
+                nextTime = nextTime.plusSeconds(increment);
+                dateTimes.add(nextTime);
+                count++;
+            }
+        }
+        final byte[] allCompressedTime = timelineCoder.compressDateTimes(dateTimes);
+        final List<DateTime> restoredTimes = timelineCoder.decompressDateTimes(allCompressedTime);
+        Assert.assertEquals(restoredTimes.size(), dateTimes.size());
+        for (int i = 0; i < count; i++) {
+            Assert.assertEquals(restoredTimes.get(i), dateTimes.get(i));
+        }
+        for (int fragmentLength = 2; fragmentLength < count / 2; fragmentLength++) {
+            final List<byte[]> fragments = new ArrayList<byte[]>();
+            final int fragmentCount = (int) Math.ceil((double) count / (double) fragmentLength);
+            for (int fragCounter = 0; fragCounter < fragmentCount; fragCounter++) {
+                final int fragIndex = fragCounter * fragmentLength;
+                final List<DateTime> fragment = dateTimes.subList(fragIndex, Math.min(count, fragIndex + fragmentLength));
+                fragments.add(timelineCoder.compressDateTimes(fragment));
+            }
+            final byte[] combined = timelineCoder.combineTimelines(fragments, null);
+            final List<DateTime> restoredDateTimes = timelineCoder.decompressDateTimes(combined);
+            //Assert.assertEquals(intTimes.length, count);
+            for (int i = 0; i < count; i++) {
+                Assert.assertEquals(restoredDateTimes.get(i), dateTimes.get(i));
+            }
+        }
+    }
+
+    @Test(groups = "fast")
+    public void test65KRepeats() throws Exception {
+        final int count = 0;
+        final List<DateTime> dateTimes = new ArrayList<DateTime>();
+        DateTime time = DateTimeUtils.dateTimeFromUnixSeconds(1000000);
+        for (int i = 0; i < 20; i++) {
+            time = time.plusSeconds(200);
+            dateTimes.add(time);
+        }
+        for (int i = 0; i < 0xFFFF + 100; i++) {
+            time = time.plusSeconds(100);
+            dateTimes.add(time);
+        }
+        final byte[] timeBytes = timelineCoder.compressDateTimes(dateTimes);
+        final String hex = new String(Hex.encodeHex(timeBytes));
+        // Here are the compressed samples: ff000f4308fe13c8fdffff64fe6464
+        // Translation:
+        // [ff 00 0f 43 08] means absolution time 1000000
+        // [fe 13 c8] means repeat 19 times delta 200 seconds
+        // [fd ff ff 64] means repeat 65525 times delta 100 seconds
+        // [fe 64 64] means repeat 100 times delta 100 seconds
+        Assert.assertEquals(timeBytes, Hex.decodeHex("ff000f4308fe13c8fdffff64fe6464".toCharArray()));
+        final List<DateTime> restoredSamples = timelineCoder.decompressDateTimes(timeBytes);
+        Assert.assertEquals(restoredSamples.size(), dateTimes.size());
+        for (int i = 0; i < count; i++) {
+            Assert.assertEquals(restoredSamples.get(i), DateTimeUtils.unixSeconds(dateTimes.get(i)));
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testCombiningTimesError() throws Exception {
+        final byte[] times1 = Hex.decodeHex("ff10000001fe0310ff1000011bfe0310".toCharArray());
+        final byte[] times2 = Hex.decodeHex("ff10000160".toCharArray());
+        final List<byte[]> timesList = new ArrayList<byte[]>();
+        timesList.add(times1);
+        timesList.add(times2);
+        final byte[] combinedTimes = timelineCoder.combineTimelines(timesList, null);
+        final String hexCombinedTimes = new String(Hex.encodeHex(combinedTimes));
+        //System.out.printf("Combined times: %s\n", hexCombinedTimes);
+        Assert.assertEquals(hexCombinedTimes, "ff10000001fe0310eafe031015");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCursorWithZeroDeltaWithNext() throws Exception {
+        // This caused a TimeCursor problem
+        // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E FF 79 0B 44 22
+        // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E
+        // FF 4F 91 D5 BC          Absolute time
+        // FE 02 1E                Repeated delta time: count 2, delta: 30
+        // 00                      Delta 0.  Why did this happen?
+        // FE 02 1E                Repeated delta time: count 2, delta: 30
+        // FF 79 0B 44 22          Absolute time
+        // Total samples: 6
+        final int sampleCount = 7;
+        final byte[] times = Hex.decodeHex("FF4F91D5BCFE021E00FE021EFF790B4422".toCharArray());
+        final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+        for (int i = 0; i < sampleCount; i++) {
+            final DateTime nextTime = cursor.getNextTime();
+            Assert.assertNotNull(nextTime);
+        }
+        try {
+            final DateTime lastTime = cursor.getNextTime();
+            Assert.fail();
+        } catch (Exception e) {
+            Assert.assertTrue(true);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCursorWithZeroDeltaWithSampleSkip() throws Exception {
+        // This caused a TimeCursor problem
+        // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E FF 79 0B 44 22
+        // FF 4F 91 D5 BC FE 02 1E 00 FE 02 1E
+        // FF 4F 91 D5 BC          Absolute time
+        // FE 02 1E                Repeated delta time: count 2, delta: 30
+        // 00                      Delta 0.  Why did this happen?
+        // FE 02 1E                Repeated delta time: count 2, delta: 30
+        // FF 79 0B 44 22          Absolute time
+        // Total samples: 6
+        final int sampleCount = 7;
+        final byte[] times = Hex.decodeHex("FF4F91D5BCFE021E00FE021EFF790B4422".toCharArray());
+        final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+        for (int i = 0; i < sampleCount; i++) {
+            final DateTime nextTime = cursor.getNextTime();
+            Assert.assertNotNull(nextTime);
+            cursor.skipToSampleNumber(i + 1);
+        }
+        try {
+            final DateTime lastTime = cursor.getNextTime();
+            Assert.fail();
+        } catch (Exception e) {
+            Assert.assertTrue(true);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCursorThatShowedError() throws Exception {
+        // 39 bytes are: ff4f90f67afd03ce1e1ffe1a1e1d01fe771e1d01fd01df1e1d1ffe761e1d01fe771e1d01fe571e
+        // 1944 samples; error at 1934
+        final int sampleCount = 1944;
+        //final byte[] times = Hex.decodeHex("ff4f90f67afd03ce1e1ffe1a1e1d01fe771e1d01fd01df1e1d1ffe761e1d01fe771e1d01fe571e".toCharArray());
+        final byte[] times = Hex.decodeHex("00000018FF4F8FE521FD023D1E1FFEF01E1D01FE771E1D01FD03E21EFE07980F".toCharArray());
+        Assert.assertEquals(times.length, 32);
+        final DefaultTimelineCursor cursor = new DefaultTimelineCursor(times, sampleCount);
+        for (int i = 0; i < sampleCount; i++) {
+            final DateTime nextTime = cursor.getNextTime();
+            Assert.assertNotNull(nextTime);
+            cursor.skipToSampleNumber(i + 1);
+        }
+
+        try {
+            final DateTime lastTime = cursor.getNextTime();
+            Assert.fail();
+        } catch (Exception e) {
+            Assert.assertTrue(true);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError1() throws Exception {
+        checkCombinedTimelines("ff4f91fb14fe631e00fe151e", "ff4f920942");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError2() throws Exception {
+        checkCombinedTimelines("ff4f922618fe111e78fe111efe02005a", "ff4f923428");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError3() throws Exception {
+        checkCombinedTimelines("ff4f9224ecfe091e", "ff4f922618fe071e78fe111e78fe111e78fe111e78fe111efe02005afe121e78fe031e",
+                               "ff4f923428fe0d1e7dfe111e78fe111e78fe111e78fe0b1e00fe061e78fe111e", "ff4f92427cfe111e78fe111e78fe111e78fe111e82fe041e1d01fe0c1e78fe0e1e");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError4() throws Exception {
+        checkCombinedTimelines("ff4f95ba83fe021e", "ff4f95d595", "ff4f95e297fe021e");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError5() throws Exception {
+        checkCombinedTimelines("ff00000100", "ff00000200");
+    }
+
+    @Test(groups = "fast")
+    public void testTimeCombineTimesError6() throws Exception {
+        checkCombinedTimelines("ff4f95ac73fe471e00fe301e", "ff4f95ba83fe471e00fe311e", "ff4f95d595", "ff4f95e297fe091e");
+        checkCombinedTimelines("ff4f95ac7afe461e1d01fe301e", "ff4f95ba8afe471e00fe041e1ffe2b1e", "ff4f95d59d", "ff4f95e281fe0a1e");
+        checkCombinedTimelines("ff4f95aca4fe461e00fe311e", "ff4f95bab4fe461e00fe261e1f1dfe0a1e", "ff4f95d5a8", "ff4f95e28cfe091e");
+        checkCombinedTimelines("ff4f95ac88fe471e00fe311e", "ff4f95bab6fe461e00fe321e", "ff4f95d5aa", "ff4f95e28efe091eff4f95e4e6fe0a1e");
+        checkCombinedTimelines("ff4f95e394ff4f95e4fcfe0e1e5afe341e00fe221e", "ff4f95f12cfe551e00fe221e", "ff4f95ff3cfe551e00fe231e", "ff4f960d6afe541e00fe231e");
+        checkCombinedTimelines("ff4f95e396ff4f95e4fefe0e1e5afe341e00fe271e", "ff4f95f1c4fe501e00fe281e", "ff4f95fff2fe4f1e00fe281e", "ff4f960e02fe4f1e00fe291e");
+    }
+
+    private void checkCombinedTimelines(final String... timelines) throws Exception {
+        final List<byte[]> timeParts = new ArrayList<byte[]>();
+        for (final String timeline : timelines) {
+            timeParts.add(Hex.decodeHex(timeline.toCharArray()));
+        }
+        int sampleCount = 0;
+        int byteCount = 0;
+        for (final byte[] timePart : timeParts) {
+            byteCount += timePart.length;
+            sampleCount += timelineCoder.countTimeBytesSamples(timePart);
+        }
+        final byte[] concatedTimes = new byte[byteCount];
+        int offset = 0;
+        for (final byte[] timePart : timeParts) {
+            final int length = timePart.length;
+            System.arraycopy(timePart, 0, concatedTimes, offset, length);
+            offset += length;
+        }
+        final byte[] newCombined = timelineCoder.combineTimelines(timeParts, null);
+        final int newCombinedLength = timelineCoder.countTimeBytesSamples(newCombined);
+        final DefaultTimelineCursor concatedCursor = new DefaultTimelineCursor(concatedTimes, sampleCount);
+        final DefaultTimelineCursor combinedCursor = new DefaultTimelineCursor(newCombined, sampleCount);
+        for (int i = 0; i < sampleCount; i++) {
+            final DateTime concatedTime = concatedCursor.getNextTime();
+            final DateTime combinedTime = combinedCursor.getNextTime();
+            Assert.assertEquals(combinedTime, concatedTime);
+        }
+        Assert.assertEquals(newCombinedLength, sampleCount);
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java
new file mode 100644
index 0000000..25c27bb
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/timeline/times/TestTimelineOpcode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage.timeline.times;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.usage.UsageTestSuite;
+
+public class TestTimelineOpcode extends UsageTestSuite {
+
+    @Test(groups = "fast")
+    public void testMaxDeltaTime() throws Exception {
+        for (final TimelineOpcode opcode : TimelineOpcode.values()) {
+            Assert.assertTrue(opcode.getOpcodeIndex() >= TimelineOpcode.MAX_DELTA_TIME);
+        }
+    }
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java b/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java
new file mode 100644
index 0000000..4c4873e
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/UsageTestSuite.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage;
+
+import com.ning.billing.KillbillTestSuite;
+
+public class UsageTestSuite extends KillbillTestSuite {
+
+}
diff --git a/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java b/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java
new file mode 100644
index 0000000..20e51c6
--- /dev/null
+++ b/usage/src/test/java/com/ning/billing/usage/UsageTestSuiteWithEmbeddedDB.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.usage;
+
+import com.ning.billing.KillbillTestSuiteWithEmbeddedDB;
+
+public class UsageTestSuiteWithEmbeddedDB extends KillbillTestSuiteWithEmbeddedDB {
+
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java b/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java
new file mode 100644
index 0000000..6af61ea
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/api/DefaultAuditUserApi.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit.api;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.dao.AuditDao;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultAuditUserApi implements AuditUserApi {
+
+    private final AuditDao auditDao;
+
+    @Inject
+    public DefaultAuditUserApi(final AuditDao auditDao) {
+        this.auditDao = auditDao;
+    }
+
+    @Override
+    public List<AuditLog> getAuditLogs(final UUID objectId, final ObjectType objectType) {
+        final TableName tableName = getTableNameFromObjectType(objectType);
+        if (tableName == null) {
+            return ImmutableList.<AuditLog>of();
+        }
+
+        return auditDao.getAuditLogsForRecordId(tableName, objectId);
+    }
+
+    private TableName getTableNameFromObjectType(final ObjectType objectType) {
+        for (final TableName tableName : TableName.values()) {
+            if (objectType.equals(tableName.getObjectType())) {
+                return tableName;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java b/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java
new file mode 100644
index 0000000..224b78b
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/dao/AuditDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.TableName;
+
+public interface AuditDao {
+
+    public List<AuditLog> getAuditLogsForRecordId(final TableName tableName, final UUID objectId);
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java b/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java
new file mode 100644
index 0000000..6d4a827
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/dao/DefaultAuditDao.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit.dao;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+
+import org.skife.jdbi.v2.IDBI;
+
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.dao.AuditSqlDao;
+import com.ning.billing.util.dao.TableName;
+
+import com.google.common.collect.ImmutableList;
+
+public class DefaultAuditDao implements AuditDao {
+
+    private final AuditSqlDao auditSqlDao;
+
+    @Inject
+    public DefaultAuditDao(final IDBI dbi) {
+        this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
+    }
+
+    @Override
+    public List<AuditLog> getAuditLogsForRecordId(final TableName tableName, final UUID objectId) {
+        final Long recordId = auditSqlDao.getRecordIdForTable(tableName.getTableName().toLowerCase(), objectId.toString());
+        if (recordId == null) {
+            return ImmutableList.<AuditLog>of();
+        } else {
+            return auditSqlDao.getAuditLogsForRecordId(tableName, recordId);
+        }
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java b/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java
new file mode 100644
index 0000000..513697e
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/audit/DefaultAuditLog.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit;
+
+import org.joda.time.DateTime;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.dao.EntityAudit;
+
+public class DefaultAuditLog implements AuditLog {
+
+    private final EntityAudit entityAudit;
+    private final CallContext callContext;
+
+    public DefaultAuditLog(final EntityAudit entityAudit, final CallContext callContext) {
+        this.entityAudit = entityAudit;
+        this.callContext = callContext;
+    }
+
+    @Override
+    public ChangeType getChangeType() {
+        return entityAudit.getChangeType();
+    }
+
+    @Override
+    public String getUserName() {
+        return callContext.getUserName();
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return callContext.getCreatedDate();
+    }
+
+    @Override
+    public String getReasonCode() {
+        return callContext.getReasonCode();
+    }
+
+    @Override
+    public String getUserToken() {
+        if (callContext.getUserToken() == null) {
+            return null;
+        } else {
+            return callContext.getUserToken().toString();
+        }
+    }
+
+    @Override
+    public String getComment() {
+        return callContext.getComment();
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("DefaultAuditLog");
+        sb.append("{entityAudit=").append(entityAudit);
+        sb.append(", callContext=").append(callContext);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultAuditLog that = (DefaultAuditLog) o;
+
+        if (callContext != null ? !callContext.equals(that.callContext) : that.callContext != null) {
+            return false;
+        }
+        if (entityAudit != null ? !entityAudit.equals(that.entityAudit) : that.entityAudit != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = entityAudit != null ? entityAudit.hashCode() : 0;
+        result = 31 * result + (callContext != null ? callContext.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java b/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
index 40d7ef5..2c21d05 100644
--- a/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
+++ b/util/src/main/java/com/ning/billing/util/callcontext/CallContextBase.java
@@ -19,12 +19,13 @@ package com.ning.billing.util.callcontext;
 import java.util.UUID;
 
 public abstract class CallContextBase implements CallContext {
-    private final UUID userToken;
-    private final String userName;
-    private final CallOrigin callOrigin;
-    private final UserType userType;
-    private final String reasonCode;
-    private final String comment;
+
+    protected final UUID userToken;
+    protected final String userName;
+    protected final CallOrigin callOrigin;
+    protected final UserType userType;
+    protected final String reasonCode;
+    protected final String comment;
 
     public CallContextBase(final String userName, final CallOrigin callOrigin, final UserType userType) {
         this(userName, callOrigin, userType, null);
diff --git a/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java b/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
index 081cd7e..e294d30 100644
--- a/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
+++ b/util/src/main/java/com/ning/billing/util/callcontext/DefaultCallContext.java
@@ -23,31 +23,102 @@ import org.joda.time.DateTime;
 import com.ning.billing.util.clock.Clock;
 
 public class DefaultCallContext extends CallContextBase {
-    private final Clock clock;
+
+    private final DateTime createdDate;
 
     public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType, final UUID userToken, final Clock clock) {
         super(userName, callOrigin, userType, userToken);
-        this.clock = clock;
+        this.createdDate = clock.getUTCNow();
     }
 
     public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType,
                               final String reasonCode, final String comment,
                               final UUID userToken, final Clock clock) {
         super(userName, callOrigin, userType, reasonCode, comment, userToken);
-        this.clock = clock;
+        this.createdDate = clock.getUTCNow();
     }
 
     public DefaultCallContext(final String userName, final CallOrigin callOrigin, final UserType userType, final Clock clock) {
         this(userName, callOrigin, userType, null, clock);
     }
 
+    public DefaultCallContext(final String userName, final DateTime createdDate, final String reasonCode,
+                              final String comment, final UUID userToken) {
+        super(userName, null, null, reasonCode, comment, userToken);
+        this.createdDate = createdDate;
+    }
+
     @Override
     public DateTime getCreatedDate() {
-        return clock.getUTCNow();
+        return createdDate;
     }
 
     @Override
     public DateTime getUpdatedDate() {
-        return clock.getUTCNow();
+        return createdDate;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("CallContextBase");
+        sb.append("{userToken=").append(userToken);
+        sb.append(", userName='").append(userName).append('\'');
+        sb.append(", callOrigin=").append(callOrigin);
+        sb.append(", userType=").append(userType);
+        sb.append(", reasonCode='").append(reasonCode).append('\'');
+        sb.append(", comment='").append(comment).append('\'');
+        sb.append(", createdDate='").append(createdDate).append('\'');
+        sb.append(", updatedDate='").append(createdDate).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultCallContext that = (DefaultCallContext) o;
+
+        if (callOrigin != that.callOrigin) {
+            return false;
+        }
+        if (comment != null ? !comment.equals(that.comment) : that.comment != null) {
+            return false;
+        }
+        if (reasonCode != null ? !reasonCode.equals(that.reasonCode) : that.reasonCode != null) {
+            return false;
+        }
+        if (userName != null ? !userName.equals(that.userName) : that.userName != null) {
+            return false;
+        }
+        if (userToken != null ? !userToken.equals(that.userToken) : that.userToken != null) {
+            return false;
+        }
+        if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+            return false;
+        }
+        if (userType != that.userType) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = userToken != null ? userToken.hashCode() : 0;
+        result = 31 * result + (userName != null ? userName.hashCode() : 0);
+        result = 31 * result + (callOrigin != null ? callOrigin.hashCode() : 0);
+        result = 31 * result + (userType != null ? userType.hashCode() : 0);
+        result = 31 * result + (reasonCode != null ? reasonCode.hashCode() : 0);
+        result = 31 * result + (comment != null ? comment.hashCode() : 0);
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        return result;
     }
 }
diff --git a/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java b/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java
new file mode 100644
index 0000000..188ea71
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/dao/AuditLogMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.audit.DefaultAuditLog;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+
+public class AuditLogMapper extends MapperBase implements ResultSetMapper<AuditLog> {
+
+    @Override
+    public AuditLog map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        final String tableName = r.getString("table_name");
+        final long recordId = r.getLong("record_id");
+        final String changeType = r.getString("change_type");
+        final DateTime changeDate = getDateTime(r, "change_date");
+        final String changedBy = r.getString("changed_by");
+        final String reasonCode = r.getString("reason_code");
+        final String comments = r.getString("comments");
+        final UUID userToken = getUUID(r, "user_token");
+
+        final EntityAudit entityAudit = new EntityAudit(TableName.valueOf(tableName), recordId, ChangeType.valueOf(changeType));
+        final CallContext callContext = new DefaultCallContext(changedBy, changeDate, reasonCode, comments, userToken);
+        return new DefaultAuditLog(entityAudit, callContext);
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java b/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
index 4f9c256..58a08c6 100644
--- a/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/dao/AuditSqlDao.java
@@ -22,13 +22,18 @@ import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlBatch;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.Define;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
 import org.skife.jdbi.v2.sqlobject.stringtemplate.ExternalizedSqlViaStringTemplate3;
 
+import com.ning.billing.util.audit.AuditLog;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallContextBinder;
 
 @ExternalizedSqlViaStringTemplate3
+@RegisterMapper(AuditLogMapper.class)
 public interface AuditSqlDao {
+
     @SqlUpdate
     public void insertAuditFromTransaction(@AuditBinder final EntityAudit audit,
                                            @CallContextBinder final CallContext context);
@@ -38,9 +43,17 @@ public interface AuditSqlDao {
                                            @CallContextBinder final CallContext context);
 
     @SqlQuery
+    public List<AuditLog> getAuditLogsForRecordId(@TableNameBinder final TableName tableName,
+                                                  @Bind("recordId") final long recordId);
+
+    @SqlQuery
     public Long getRecordId(@Bind("id") final String id);
 
     @SqlQuery
+    public Long getRecordIdForTable(@Define("tableName") final String tableName,
+                                    @Bind("id") final String id);
+
+    @SqlQuery
     public Long getHistoryRecordId(@Bind("recordId") final Long recordId);
 
 }
diff --git a/util/src/main/java/com/ning/billing/util/dao/TableName.java b/util/src/main/java/com/ning/billing/util/dao/TableName.java
index a5038d5..4dedcd0 100644
--- a/util/src/main/java/com/ning/billing/util/dao/TableName.java
+++ b/util/src/main/java/com/ning/billing/util/dao/TableName.java
@@ -16,31 +16,40 @@
 
 package com.ning.billing.util.dao;
 
+import javax.annotation.Nullable;
+
 public enum TableName {
-    ACCOUNT("accounts"),
-    ACCOUNT_HISTORY("account_history"),
-    ACCOUNT_EMAIL_HISTORY("account_email_history"),
-    BUNDLES("bundles"),
-    CUSTOM_FIELD_HISTORY("custom_field_history"),
-    INVOICE_ITEMS("invoice_items"),
-    INVOICE_PAYMENTS("invoice_payments"),
-    INVOICES("invoices"),
-    PAYMENT_ATTEMPTS("payment_attempts"),
-    PAYMENT_HISTORY("payment_history"),
-    PAYMENTS("payments"),
-    PAYMENT_METHODS("payment_methods"),
-    SUBSCRIPTIONS("subscriptions"),
-    SUBSCRIPTION_EVENTS("subscription_events"),
-    REFUNDS("refunds"),
-    TAG_HISTORY("tag_history");
+    ACCOUNT("accounts", ObjectType.ACCOUNT),
+    ACCOUNT_HISTORY("account_history", null),
+    ACCOUNT_EMAIL_HISTORY("account_email_history", ObjectType.ACCOUNT_EMAIL),
+    BUNDLES("bundles", ObjectType.BUNDLE),
+    CUSTOM_FIELD_HISTORY("custom_field_history", null),
+    INVOICE_ITEMS("invoice_items", ObjectType.INVOICE_ITEM),
+    INVOICE_PAYMENTS("invoice_payments", ObjectType.INVOICE_PAYMENT),
+    INVOICES("invoices", ObjectType.INVOICE),
+    PAYMENT_ATTEMPTS("payment_attempts", null),
+    PAYMENT_HISTORY("payment_history", null),
+    PAYMENTS("payments", ObjectType.PAYMENT),
+    PAYMENT_METHODS("payment_methods", ObjectType.PAYMENT_METHOD),
+    SUBSCRIPTIONS("subscriptions", ObjectType.SUBSCRIPTION),
+    SUBSCRIPTION_EVENTS("subscription_events", null),
+    REFUNDS("refunds", ObjectType.REFUND),
+    TAG_DEFINITIONS("tag_definitions", ObjectType.TAG_DEFINITION),
+    TAG_HISTORY("tag_history", null);
 
     private final String tableName;
+    private final ObjectType objectType;
 
-    TableName(final String tableName) {
+    TableName(final String tableName, @Nullable final ObjectType objectType) {
         this.tableName = tableName;
+        this.objectType = objectType;
     }
 
     public String getTableName() {
         return tableName;
     }
+
+    public ObjectType getObjectType() {
+        return objectType;
+    }
 }
diff --git a/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java b/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
index 545186c..9cacf92 100644
--- a/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
+++ b/util/src/main/java/com/ning/billing/util/dao/TableNameBinder.java
@@ -31,7 +31,9 @@ import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.PARAMETER})
 public @interface TableNameBinder {
+
     public static class TableNameBinderFactory implements BinderFactory {
+
         public Binder build(final Annotation annotation) {
             return new Binder<TableNameBinder, TableName>() {
                 public void bind(final SQLStatement q, final TableNameBinder bind, final TableName tableName) {
diff --git a/util/src/main/java/com/ning/billing/util/glue/AuditModule.java b/util/src/main/java/com/ning/billing/util/glue/AuditModule.java
new file mode 100644
index 0000000..2fa9310
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/glue/AuditModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.glue;
+
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.audit.api.DefaultAuditUserApi;
+import com.ning.billing.util.audit.dao.AuditDao;
+import com.ning.billing.util.audit.dao.DefaultAuditDao;
+
+import com.google.inject.AbstractModule;
+
+public class AuditModule extends AbstractModule {
+
+    protected void installDaos() {
+        bind(AuditDao.class).to(DefaultAuditDao.class).asEagerSingleton();
+    }
+
+    protected void installUserApi() {
+        bind(AuditUserApi.class).to(DefaultAuditUserApi.class).asEagerSingleton();
+    }
+
+    @Override
+    protected void configure() {
+        installDaos();
+        installUserApi();
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java b/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java
new file mode 100644
index 0000000..8ce34d0
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/audit/dao/TestDefaultAuditDao.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit.dao;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.IDBI;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.UtilTestSuiteWithEmbeddedDB;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.bus.Bus;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContextFactory;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.dao.ObjectType;
+import com.ning.billing.util.dao.TableName;
+import com.ning.billing.util.glue.AuditModule;
+import com.ning.billing.util.tag.MockTagStoreModuleSql;
+import com.ning.billing.util.tag.TagDefinition;
+import com.ning.billing.util.tag.dao.AuditedTagDao;
+import com.ning.billing.util.tag.dao.TagDefinitionDao;
+
+import com.google.inject.Inject;
+
+@Guice(modules = {MockTagStoreModuleSql.class, AuditModule.class})
+public class TestDefaultAuditDao extends UtilTestSuiteWithEmbeddedDB {
+
+    @Inject
+    private TagDefinitionDao tagDefinitionDao;
+
+    @Inject
+    private AuditedTagDao tagDao;
+
+    @Inject
+    private AuditDao auditDao;
+
+    @Inject
+    private Clock clock;
+
+    @Inject
+    private Bus bus;
+
+    @Inject
+    private IDBI dbi;
+
+    private CallContext context;
+
+    @BeforeClass(groups = "slow")
+    public void setup() throws IOException {
+        context = new DefaultCallContextFactory(clock).createCallContext("Audit DAO test", CallOrigin.TEST, UserType.TEST, UUID.randomUUID());
+        bus.start();
+    }
+
+    @AfterClass(groups = "slow")
+    public void tearDown() {
+        bus.stop();
+    }
+
+    @Test(groups = "slow")
+    public void testRetrieveAudits() throws Exception {
+        final TagDefinition defYo = tagDefinitionDao.create("yo", "defintion yo", context);
+
+        // Create a tag
+        tagDao.insertTag(UUID.randomUUID(), ObjectType.ACCOUNT, defYo.getId(), context);
+
+        // Verify we get an audit entry for the tag_history table
+        final Handle handle = dbi.open();
+        final String tagHistoryString = (String) handle.select("select id from tag_history limit 1").get(0).get("id");
+        handle.close();
+
+        final List<AuditLog> auditLogs = auditDao.getAuditLogsForRecordId(TableName.TAG_HISTORY, UUID.fromString(tagHistoryString));
+        Assert.assertEquals(auditLogs.size(), 1);
+        Assert.assertEquals(auditLogs.get(0).getUserToken(), context.getUserToken().toString());
+        Assert.assertEquals(auditLogs.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertNull(auditLogs.get(0).getComment());
+        Assert.assertNull(auditLogs.get(0).getReasonCode());
+        Assert.assertEquals(auditLogs.get(0).getUserName(), context.getUserName());
+        Assert.assertNotNull(auditLogs.get(0).getCreatedDate());
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java b/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java
new file mode 100644
index 0000000..709708f
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/audit/TestDefaultAuditLog.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.audit;
+
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.ChangeType;
+import com.ning.billing.util.UtilTestSuite;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
+import com.ning.billing.util.callcontext.DefaultCallContext;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.clock.ClockMock;
+import com.ning.billing.util.dao.EntityAudit;
+import com.ning.billing.util.dao.TableName;
+
+public class TestDefaultAuditLog extends UtilTestSuite {
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+        final long recordId = Long.MAX_VALUE;
+        final ChangeType changeType = ChangeType.DELETE;
+        final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+        final String userName = UUID.randomUUID().toString();
+        final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+        final UserType userType = UserType.CUSTOMER;
+        final UUID userToken = UUID.randomUUID();
+        final ClockMock clock = new ClockMock();
+        final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+        final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+        Assert.assertEquals(auditLog.getChangeType(), changeType);
+        Assert.assertNull(auditLog.getComment());
+        Assert.assertNotNull(auditLog.getCreatedDate());
+        Assert.assertNull(auditLog.getReasonCode());
+        Assert.assertEquals(auditLog.getUserName(), userName);
+        Assert.assertEquals(auditLog.getUserToken(), userToken.toString());
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final TableName tableName = TableName.ACCOUNT_EMAIL_HISTORY;
+        final long recordId = Long.MAX_VALUE;
+        final ChangeType changeType = ChangeType.DELETE;
+        final EntityAudit entityAudit = new EntityAudit(tableName, recordId, changeType);
+
+        final String userName = UUID.randomUUID().toString();
+        final CallOrigin callOrigin = CallOrigin.EXTERNAL;
+        final UserType userType = UserType.CUSTOMER;
+        final UUID userToken = UUID.randomUUID();
+        final ClockMock clock = new ClockMock();
+        final CallContext callContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+
+        final AuditLog auditLog = new DefaultAuditLog(entityAudit, callContext);
+        Assert.assertEquals(auditLog, auditLog);
+
+        final AuditLog sameAuditLog = new DefaultAuditLog(entityAudit, callContext);
+        Assert.assertEquals(sameAuditLog, auditLog);
+
+        clock.addMonths(1);
+        final CallContext otherCallContext = new DefaultCallContext(userName, callOrigin, userType, userToken, clock);
+        final AuditLog otherAuditLog = new DefaultAuditLog(entityAudit, otherCallContext);
+        Assert.assertNotEquals(otherAuditLog, auditLog);
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java b/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java
new file mode 100644
index 0000000..c56b5f3
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/callcontext/TestDefaultCallContext.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.callcontext;
+
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuite;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestDefaultCallContext extends UtilTestSuite {
+
+    private final Clock clock = new ClockMock();
+
+    @Test(groups = "fast")
+    public void testGetters() throws Exception {
+        final String userName = UUID.randomUUID().toString();
+        final DateTime createdDate = clock.getUTCNow();
+        final String reasonCode = UUID.randomUUID().toString();
+        final String comment = UUID.randomUUID().toString();
+        final UUID userToken = UUID.randomUUID();
+        final DefaultCallContext callContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+
+        Assert.assertEquals(callContext.getCreatedDate(), createdDate);
+        Assert.assertNull(callContext.getCallOrigin());
+        Assert.assertEquals(callContext.getComment(), comment);
+        Assert.assertEquals(callContext.getReasonCode(), reasonCode);
+        Assert.assertEquals(callContext.getUserName(), userName);
+        Assert.assertEquals(callContext.getUpdatedDate(), createdDate);
+        Assert.assertEquals(callContext.getUserToken(), userToken);
+        Assert.assertNull(callContext.getUserType());
+    }
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final String userName = UUID.randomUUID().toString();
+        final DateTime createdDate = clock.getUTCNow();
+        final String reasonCode = UUID.randomUUID().toString();
+        final String comment = UUID.randomUUID().toString();
+        final UUID userToken = UUID.randomUUID();
+
+        final DefaultCallContext callContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+        Assert.assertEquals(callContext, callContext);
+
+        final DefaultCallContext sameCallContext = new DefaultCallContext(userName, createdDate, reasonCode, comment, userToken);
+        Assert.assertEquals(sameCallContext, callContext);
+
+        final DefaultCallContext otherCallContext = new DefaultCallContext(UUID.randomUUID().toString(), createdDate, reasonCode, comment, userToken);
+        Assert.assertNotEquals(otherCallContext, callContext);
+    }
+}