killbill-memoizeit

Changes

.gitignore 1(+1 -0)

.idea/compiler.xml 116(+4 -112)

account/pom.xml 2(+1 -1)

api/pom.xml 2(+1 -1)

beatrix/pom.xml 2(+1 -1)

catalog/pom.xml 2(+1 -1)

currency/pom.xml 2(+1 -1)

invoice/pom.xml 2(+1 -1)

jaxrs/pom.xml 6(+1 -5)

junction/pom.xml 2(+1 -1)

NEWS 6(+6 -0)

overdue/pom.xml 2(+1 -1)

payment/pom.xml 6(+1 -5)

pom.xml 2(+1 -1)

profiles/pom.xml 2(+1 -1)

tenant/pom.xml 2(+1 -1)

usage/pom.xml 2(+1 -1)

util/pom.xml 31(+22 -9)

util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java 126(+0 -126)

util/src/main/java/org/killbill/billing/util/cache/ExpirationListenerFactory.java 89(+0 -89)

util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java 54(+0 -54)

util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java 80(+0 -80)

Details

.gitignore 1(+1 -0)

diff --git a/.gitignore b/.gitignore
index 5609cd9..fba5383 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 .idea/workspace.xml
 .idea/libraries
+.idea/kotlinc.xml
 *.ipr
 *.iws
 *.DS_Store

.idea/compiler.xml 116(+4 -112)

diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 9b1b3d8..d6c37f8 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,8 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="CompilerConfiguration">
-    <option name="DEFAULT_COMPILER" value="Javac" />
-    <resourceExtensions />
     <wildcardResourcePatterns>
       <entry name="!?*.java" />
       <entry name="!?*.form" />
@@ -14,135 +12,29 @@
       <entry name="!?*.clj" />
     </wildcardResourcePatterns>
     <annotationProcessing>
-      <profile default="true" name="Default" enabled="false">
-        <processorPath useClasspath="true" />
-      </profile>
-      <profile default="false" name="Maven default annotation processors profile" enabled="true">
+      <profile name="Maven default annotation processors profile" enabled="true">
         <sourceOutputDir name="target/generated-sources/annotations" />
         <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
         <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="currency" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-account" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-account" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-internal-api" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
-        <module name="killbill-internal-api" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-beatrix" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-beatrix" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-catalog" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-catalog" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-currency" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-currency" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-entitlement" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-entitlement" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-invoice" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
+        <module name="killbill-internal-api" />
         <module name="killbill-invoice" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-jaxrs" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-jaxrs" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-junction" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-junction" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-overdue" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-overdue" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-payment" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-payment" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-subscription" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
+        <module name="killbill-profiles-killbill" />
+        <module name="killbill-profiles-killpay" />
         <module name="killbill-subscription" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-tenant" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-tenant" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-usage" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-usage" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-util" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
         <module name="killbill-util" />
       </profile>
-      <profile default="false" name="Annotation profile for killbill-profiles-killbill" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
-        <module name="killbill-profiles-killbill" />
-      </profile>
-      <profile default="false" name="Annotation profile for killbill-profiles-killpay" enabled="true">
-        <sourceOutputDir name="target/generated-sources/annotations" />
-        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="true" />
-        <module name="killbill-profiles-killpay" />
-      </profile>
     </annotationProcessing>
     <bytecodeTargetLevel>
       <module name="currency" target="1.6" />

account/pom.xml 2(+1 -1)

diff --git a/account/pom.xml b/account/pom.xml
index d27448c..0059a35 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-account</artifactId>
diff --git a/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java b/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java
index 3bd4afc..7e24a22 100644
--- a/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java
+++ b/account/src/main/java/org/killbill/billing/account/api/DefaultImmutableAccountData.java
@@ -17,28 +17,41 @@
 
 package org.killbill.billing.account.api;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
+import org.killbill.billing.account.dao.AccountModelDao;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.util.account.AccountDateTimeUtils;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 
-public class DefaultImmutableAccountData implements ImmutableAccountData {
+public class DefaultImmutableAccountData implements ImmutableAccountData, Externalizable {
 
-    private final UUID id;
-    private final String externalKey;
-    private final Currency currency;
-    private final DateTimeZone dateTimeZone;
-    private final DateTimeZone fixedOffsetDateTimeZone;
-    private final DateTime referenceTime;
+    private static final long serialVersionUID = 8117686452347277415L;
 
-    public DefaultImmutableAccountData(final UUID id, final String externalKey, final Currency currency, final DateTimeZone dateTimeZone, final DateTimeZone fixedOffsetDateTimeZone, final DateTime referenceTime) {
+    private UUID id;
+    private String externalKey;
+    private Currency currency;
+    private DateTimeZone timeZone;
+    private DateTimeZone fixedOffsetTimeZone;
+    private DateTime referenceTime;
+
+    // For deserialization
+    public DefaultImmutableAccountData() {}
+
+    public DefaultImmutableAccountData(final UUID id, final String externalKey, final Currency currency, final DateTimeZone timeZone, final DateTimeZone fixedOffsetTimeZone, final DateTime referenceTime) {
         this.id = id;
         this.externalKey = externalKey;
         this.currency = currency;
-        this.dateTimeZone = dateTimeZone;
-        this.fixedOffsetDateTimeZone = fixedOffsetDateTimeZone;
+        this.timeZone = timeZone;
+        this.fixedOffsetTimeZone = fixedOffsetTimeZone;
         this.referenceTime = referenceTime;
     }
 
@@ -51,6 +64,15 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
              AccountDateTimeUtils.getReferenceDateTime(account));
     }
 
+    public DefaultImmutableAccountData(final AccountModelDao account) {
+        this(account.getId(),
+             account.getExternalKey(),
+             account.getCurrency(),
+             account.getTimeZone(),
+             AccountDateTimeUtils.getFixedOffsetTimeZone(account),
+             AccountDateTimeUtils.getReferenceDateTime(account));
+    }
+
     @Override
     public UUID getId() {
         return id;
@@ -68,7 +90,7 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
 
     @Override
     public DateTimeZone getTimeZone() {
-        return dateTimeZone;
+        return timeZone;
     }
 
     @Override
@@ -86,7 +108,7 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
     }
 
     public DateTimeZone getFixedOffsetTimeZone() {
-        return fixedOffsetDateTimeZone;
+        return fixedOffsetTimeZone;
     }
 
     @Override
@@ -100,8 +122,8 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
         sb.append("id=").append(id);
         sb.append(", externalKey='").append(externalKey).append('\'');
         sb.append(", currency=").append(currency);
-        sb.append(", dateTimeZone=").append(dateTimeZone);
-        sb.append(", fixedOffsetDateTimeZone=").append(fixedOffsetDateTimeZone);
+        sb.append(", timeZone=").append(timeZone);
+        sb.append(", fixedOffsetTimeZone=").append(fixedOffsetTimeZone);
         sb.append(", referenceTime=").append(referenceTime);
         sb.append('}');
         return sb.toString();
@@ -127,10 +149,10 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
         if (currency != that.currency) {
             return false;
         }
-        if (dateTimeZone != null ? !dateTimeZone.equals(that.dateTimeZone) : that.dateTimeZone != null) {
+        if (timeZone != null ? !timeZone.equals(that.timeZone) : that.timeZone != null) {
             return false;
         }
-        if (fixedOffsetDateTimeZone != null ? !fixedOffsetDateTimeZone.equals(that.fixedOffsetDateTimeZone) : that.fixedOffsetDateTimeZone != null) {
+        if (fixedOffsetTimeZone != null ? !fixedOffsetTimeZone.equals(that.fixedOffsetTimeZone) : that.fixedOffsetTimeZone != null) {
             return false;
         }
         return referenceTime != null ? referenceTime.compareTo(that.referenceTime) == 0 : that.referenceTime == null;
@@ -141,9 +163,19 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
         int result = id != null ? id.hashCode() : 0;
         result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
         result = 31 * result + (currency != null ? currency.hashCode() : 0);
-        result = 31 * result + (dateTimeZone != null ? dateTimeZone.hashCode() : 0);
-        result = 31 * result + (fixedOffsetDateTimeZone != null ? fixedOffsetDateTimeZone.hashCode() : 0);
+        result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0);
+        result = 31 * result + (fixedOffsetTimeZone != null ? fixedOffsetTimeZone.hashCode() : 0);
         result = 31 * result + (referenceTime != null ? referenceTime.hashCode() : 0);
         return result;
     }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
index c9cc74a..ba32fd5 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -56,7 +56,7 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements 
 
     private final ImmutableAccountInternalApi immutableAccountInternalApi;
     private final AccountDao accountDao;
-    private final CacheController bcdCacheController;
+    private final CacheController<UUID, Integer> bcdCacheController;
 
     @Inject
     public DefaultAccountInternalApi(final ImmutableAccountInternalApi immutableAccountInternalApi,
@@ -106,7 +106,7 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements 
     @Override
     public int getBCD(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
         final CacheLoaderArgument arg = createBCDCacheLoaderArgument(context);
-        final Integer result = (Integer) bcdCacheController.get(accountId, arg);
+        final Integer result = bcdCacheController.get(accountId, arg);
         return result != null ? result : DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
     }
 
@@ -160,8 +160,8 @@ public class DefaultAccountInternalApi extends DefaultAccountApiBase implements 
     private CacheLoaderArgument createBCDCacheLoaderArgument(final InternalTenantContext context) {
         final AccountBCDCacheLoader.LoaderCallback loaderCallback = new AccountBCDCacheLoader.LoaderCallback() {
             @Override
-            public Object loadAccountBCD(final UUID accountId, final InternalTenantContext context) {
-                Object result = accountDao.getAccountBCD(accountId, context);
+            public Integer loadAccountBCD(final UUID accountId, final InternalTenantContext context) {
+                Integer result = accountDao.getAccountBCD(accountId, context);
                 if (result != null) {
                     // If the value is 0, then account BCD was not set so we don't want to create a cache entry
                     result = result.equals(DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL) ? null : result;
diff --git a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultImmutableAccountInternalApi.java b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultImmutableAccountInternalApi.java
index 250d47a..4bdf0c6 100644
--- a/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultImmutableAccountInternalApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/svcs/DefaultImmutableAccountInternalApi.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -48,8 +48,8 @@ public class DefaultImmutableAccountInternalApi implements ImmutableAccountInter
 
     private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
     private final NonEntityDao nonEntityDao;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
-    private final CacheController accountCacheController;
+    private final CacheController<Long, ImmutableAccountData> accountCacheController;
+    private final CacheController<String, Long> recordIdCacheController;
 
     @Inject
     public DefaultImmutableAccountInternalApi(final IDBI dbi,
@@ -59,26 +59,26 @@ public class DefaultImmutableAccountInternalApi implements ImmutableAccountInter
         // This API will directly issue queries instead of relying on the DAO (introduced to avoid Guice circular dependencies with InternalCallContextFactory)
         this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao, null);
         this.nonEntityDao = nonEntityDao;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
         this.accountCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+        this.recordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
     }
 
     @Override
     public ImmutableAccountData getImmutableAccountDataById(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
-        final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+        final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, recordIdCacheController);
         return getImmutableAccountDataByRecordId(recordId, context);
     }
 
     @Override
     public ImmutableAccountData getImmutableAccountDataByRecordId(final Long recordId, final InternalTenantContext context) throws AccountApiException {
         final CacheLoaderArgument arg = createImmutableAccountCacheLoaderArgument(context);
-        return (ImmutableAccountData) accountCacheController.get(recordId, arg);
+        return accountCacheController.get(recordId, arg);
     }
 
     private CacheLoaderArgument createImmutableAccountCacheLoaderArgument(final InternalTenantContext context) {
         final LoaderCallback loaderCallback = new LoaderCallback() {
             @Override
-            public Object loadAccount(final Long recordId, final InternalTenantContext context) {
+            public ImmutableAccountData loadAccount(final Long recordId, final InternalTenantContext context) {
                 final Account account = getAccountByRecordIdInternal(recordId, context);
                 return account != null ? new DefaultImmutableAccountData(account) : null;
             }
diff --git a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java
index 7792ab4..a77c9e7 100644
--- a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java
+++ b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountApiBase.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -25,6 +25,7 @@ import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.DefaultAccount;
 import org.killbill.billing.account.api.DefaultImmutableAccountData;
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.account.dao.AccountDao;
 import org.killbill.billing.account.dao.AccountModelDao;
 import org.killbill.billing.callcontext.InternalTenantContext;
@@ -36,8 +37,8 @@ import org.killbill.billing.util.dao.NonEntityDao;
 public class DefaultAccountApiBase {
 
     private final AccountDao accountDao;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
-    private final CacheController accountCacheController;
+    private final CacheController<Long, ImmutableAccountData> accountCacheController;
+    private final CacheController<String, Long> recordIdCacheController;
     private final NonEntityDao nonEntityDao;
 
     public DefaultAccountApiBase(final AccountDao accountDao,
@@ -45,17 +46,17 @@ public class DefaultAccountApiBase {
                                  final CacheControllerDispatcher cacheControllerDispatcher) {
         this.accountDao = accountDao;
         this.nonEntityDao = nonEntityDao;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
         this.accountCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+        this.recordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
     }
 
     protected Account getAccountById(final UUID accountId, final InternalTenantContext context) throws AccountApiException {
-        final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+        final Long recordId = nonEntityDao.retrieveRecordIdFromObject(accountId, ObjectType.ACCOUNT, recordIdCacheController);
         final Account account = getAccountByRecordIdInternal(recordId, context);
         if (account == null) {
             throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, accountId);
         }
-        accountCacheController.putIfAbsent(accountId, new DefaultImmutableAccountData(account));
+        accountCacheController.putIfAbsent(recordId, new DefaultImmutableAccountData(account));
         return account;
     }
 
@@ -65,7 +66,8 @@ public class DefaultAccountApiBase {
             throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_KEY, key);
         }
         final Account account = new DefaultAccount(accountModelDao);
-        accountCacheController.putIfAbsent(account.getId(), new DefaultImmutableAccountData(account));
+        final Long recordId = nonEntityDao.retrieveRecordIdFromObject(account.getId(), ObjectType.ACCOUNT, recordIdCacheController);
+        accountCacheController.putIfAbsent(recordId, new DefaultImmutableAccountData(account));
         return account;
     }
 
diff --git a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
index a164bef..c1474e4 100644
--- a/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
+++ b/account/src/main/java/org/killbill/billing/account/api/user/DefaultAccountUserApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -29,6 +29,8 @@ import org.killbill.billing.account.api.AccountEmail;
 import org.killbill.billing.account.api.AccountUserApi;
 import org.killbill.billing.account.api.DefaultAccount;
 import org.killbill.billing.account.api.DefaultAccountEmail;
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.account.dao.AccountDao;
 import org.killbill.billing.account.dao.AccountEmailModelDao;
 import org.killbill.billing.account.dao.AccountModelDao;
@@ -51,15 +53,18 @@ import static org.killbill.billing.util.entity.dao.DefaultPaginationHelper.getEn
 
 public class DefaultAccountUserApi extends DefaultAccountApiBase implements AccountUserApi {
 
+    private final ImmutableAccountInternalApi immutableAccountInternalApi;
     private final InternalCallContextFactory internalCallContextFactory;
     private final AccountDao accountDao;
 
     @Inject
-    public DefaultAccountUserApi(final AccountDao accountDao,
+    public DefaultAccountUserApi(final ImmutableAccountInternalApi immutableAccountInternalApi,
+                                 final AccountDao accountDao,
                                  final NonEntityDao nonEntityDao,
                                  final CacheControllerDispatcher cacheControllerDispatcher,
                                  final InternalCallContextFactory internalCallContextFactory) {
         super(accountDao, nonEntityDao, cacheControllerDispatcher);
+        this.immutableAccountInternalApi = immutableAccountInternalApi;
         this.internalCallContextFactory = internalCallContextFactory;
         this.accountDao = accountDao;
     }
@@ -89,7 +94,10 @@ public class DefaultAccountUserApi extends DefaultAccountApiBase implements Acco
 
         if (data.getParentAccountId() != null) {
             // verify that parent account exists if parentAccountId is not null
-            getAccountById(data.getParentAccountId(), internalContext);
+            final ImmutableAccountData immutableAccountData = immutableAccountInternalApi.getImmutableAccountDataById(data.getParentAccountId(), internalContext);
+            if (immutableAccountData == null) {
+                throw new AccountApiException(ErrorCode.ACCOUNT_DOES_NOT_EXIST_FOR_ID, data.getParentAccountId());
+            }
         }
 
         final AccountModelDao account = new AccountModelDao(data);
diff --git a/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java b/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
index 91fe1ce..b0e392f 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/AccountModelDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -31,12 +31,13 @@ import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.dao.TableName;
 import org.killbill.billing.util.entity.dao.EntityModelDao;
 import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
+import org.killbill.billing.util.entity.dao.TimeZoneAwareEntity;
 
 import com.google.common.base.MoreObjects;
 
 import static org.killbill.billing.account.api.DefaultMutableAccountData.DEFAULT_BILLING_CYCLE_DAY_LOCAL;
 
-public class AccountModelDao extends EntityModelDaoBase implements EntityModelDao<Account> {
+public class AccountModelDao extends EntityModelDaoBase implements TimeZoneAwareEntity, EntityModelDao<Account> {
 
     private String externalKey;
     private String email;
@@ -133,6 +134,15 @@ public class AccountModelDao extends EntityModelDaoBase implements EntityModelDa
         this(UUIDs.randomUUID(), account);
     }
 
+    @Override
+    public void setRecordId(final Long recordId) {
+        super.setRecordId(recordId);
+        // Invoked by the jDBI mapper when retrieving the record: while there is no account_record_id column,
+        // populate the field manually for EntitySqlDaoWrapperInvocationHandler#populateCaches to populate the
+        // ACCOUNT_RECORD_ID cache
+        setAccountRecordId(recordId);
+    }
+
     public String getExternalKey() {
         return externalKey;
     }
@@ -205,6 +215,7 @@ public class AccountModelDao extends EntityModelDaoBase implements EntityModelDa
         this.paymentMethodId = paymentMethodId;
     }
 
+    @Override
     public DateTimeZone getTimeZone() {
         return timeZone;
     }
diff --git a/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java b/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
index 2eccd3c..77bea77 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/AccountSqlDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -44,7 +46,7 @@ public interface AccountSqlDao extends EntitySqlDao<AccountModelDao, Account> {
 
     @SqlQuery
     public Integer getBCD(@Bind("id") String accountId,
-                       @BindBean final InternalTenantContext context);
+                          @BindBean final InternalTenantContext context);
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
@@ -53,9 +55,9 @@ public interface AccountSqlDao extends EntitySqlDao<AccountModelDao, Account> {
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
-    public void updatePaymentMethod(@Bind("id") String accountId,
-                                    @Bind("paymentMethodId") String paymentMethodId,
-                                    @BindBean final InternalCallContext context);
+    public Object updatePaymentMethod(@Bind("id") String accountId,
+                                      @Bind("paymentMethodId") String paymentMethodId,
+                                      @BindBean final InternalCallContext context);
 
     @SqlQuery
     List<AccountModelDao> getAccountsByParentId(@Bind("parentAccountId") UUID parentAccountId,
diff --git a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
index 6ca2fa3..3054b9e 100644
--- a/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
+++ b/account/src/main/java/org/killbill/billing/account/dao/DefaultAccountDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -26,6 +26,8 @@ import org.killbill.billing.BillingExceptionBase;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.DefaultImmutableAccountData;
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.account.api.user.DefaultAccountChangeEvent;
 import org.killbill.billing.account.api.user.DefaultAccountCreationEvent;
 import org.killbill.billing.account.api.user.DefaultAccountCreationEvent.DefaultAccountData;
@@ -35,6 +37,8 @@ import org.killbill.billing.entity.EntityPersistenceException;
 import org.killbill.billing.events.AccountChangeInternalEvent;
 import org.killbill.billing.events.AccountCreationInternalEvent;
 import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.NonEntityDao;
@@ -60,6 +64,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
 
     private static final Logger log = LoggerFactory.getLogger(DefaultAccountDao.class);
 
+    private final CacheController<Long, ImmutableAccountData> accountImmutableCacheController;
     private final PersistentBus eventBus;
     private final InternalCallContextFactory internalCallContextFactory;
     private final Clock clock;
@@ -68,12 +73,22 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
     public DefaultAccountDao(final IDBI dbi, final PersistentBus eventBus, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher,
                              final InternalCallContextFactory internalCallContextFactory, final NonEntityDao nonEntityDao) {
         super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao, internalCallContextFactory), AccountSqlDao.class);
+        this.accountImmutableCacheController  = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
         this.eventBus = eventBus;
         this.internalCallContextFactory = internalCallContextFactory;
         this.clock = clock;
     }
 
     @Override
+    public void create(final AccountModelDao entity, final InternalCallContext context) throws AccountApiException {
+        final AccountModelDao refreshedEntity = transactionalSqlDao.execute(getCreateEntitySqlDaoTransactionWrapper(entity, context));
+        // Populate the caches only after the transaction has been committed, in case of rollbacks
+        transactionalSqlDao.populateCaches(refreshedEntity);
+        // Eagerly populate the account-immutable cache as well
+        accountImmutableCacheController.putIfAbsent(refreshedEntity.getRecordId(), new DefaultImmutableAccountData(refreshedEntity));
+    }
+
+    @Override
     protected AccountApiException generateAlreadyExistsException(final AccountModelDao account, final InternalCallContext context) {
         return new AccountApiException(ErrorCode.ACCOUNT_ALREADY_EXISTS, account.getExternalKey());
     }
@@ -89,9 +104,9 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                 return;
         }
 
-        final Long recordId = entitySqlDaoWrapperFactory.become(AccountSqlDao.class).getRecordId(savedAccount.getId().toString(), context);
+        final Long recordId = savedAccount.getRecordId();
         // We need to re-hydrate the callcontext with the account record id
-        final InternalCallContext rehydratedContext = internalCallContextFactory.createInternalCallContext(recordId, context);
+        final InternalCallContext rehydratedContext = internalCallContextFactory.createInternalCallContext(savedAccount, recordId, context);
         final AccountCreationInternalEvent creationEvent = new DefaultAccountCreationEvent(new DefaultAccountData(savedAccount), savedAccount.getId(),
                                                                                            rehydratedContext.getAccountRecordId(), rehydratedContext.getTenantRecordId(), rehydratedContext.getUserToken());
         try {
@@ -215,9 +230,8 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                 }
 
                 final String thePaymentMethodId = paymentMethodId != null ? paymentMethodId.toString() : null;
-                transactional.updatePaymentMethod(accountId.toString(), thePaymentMethodId, context);
+                final AccountModelDao account = (AccountModelDao) transactional.updatePaymentMethod(accountId.toString(), thePaymentMethodId, context);
 
-                final AccountModelDao account = transactional.getById(accountId.toString(), context);
                 final AccountChangeInternalEvent changeEvent = new DefaultAccountChangeEvent(accountId, currentAccount, account,
                                                                                              context.getAccountRecordId(),
                                                                                              context.getTenantRecordId(),
@@ -245,7 +259,7 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
                     throw new AccountApiException(ErrorCode.ACCOUNT_EMAIL_ALREADY_EXISTS, email.getId());
                 }
 
-                transactional.create(email, context);
+                createAndRefresh(transactional, email, context);
                 return null;
             }
         });
diff --git a/account/src/test/java/org/killbill/billing/account/AccountTestSuiteNoDB.java b/account/src/test/java/org/killbill/billing/account/AccountTestSuiteNoDB.java
index 807e5e1..447b18d 100644
--- a/account/src/test/java/org/killbill/billing/account/AccountTestSuiteNoDB.java
+++ b/account/src/test/java/org/killbill/billing/account/AccountTestSuiteNoDB.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,6 +18,7 @@
 
 package org.killbill.billing.account;
 
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
@@ -43,6 +46,8 @@ public abstract class AccountTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     @Inject
     protected AccountDao accountDao;
     @Inject
+    protected ImmutableAccountInternalApi immutableAccountInternalApi;
+    @Inject
     protected AccountUserApi accountUserApi;
     @Inject
     protected AuditDao auditDao;
diff --git a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApi.java b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApi.java
index 9fe5b8c..b5dbac0 100644
--- a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApi.java
+++ b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApi.java
@@ -350,12 +350,10 @@ public class TestDefaultAccountUserApi extends AccountTestSuiteWithEmbeddedDB {
     @Test(groups = "slow", description = "Test Account create Child with a non existing Parent",
             expectedExceptions = AccountApiException.class, expectedExceptionsMessageRegExp = "Account does not exist for id .*")
     public void testCreateChildAccountWithInvalidParent() throws Exception {
-
         final AccountModelDao childAccountModel = createTestAccount();
         childAccountModel.setParentAccountId(UUID.randomUUID());
         final AccountData childAccountData = new DefaultAccount(childAccountModel);
         final Account childAccount = accountUserApi.createAccount(childAccountData, callContext);
-
     }
 
     @Test(groups = "slow", description = "Test un- and re-parenting")
diff --git a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
index 793d689..44e370c 100644
--- a/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
+++ b/account/src/test/java/org/killbill/billing/account/api/user/TestDefaultAccountUserApiWithMocks.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -53,7 +53,7 @@ public class TestDefaultAccountUserApiWithMocks extends AccountTestSuiteNoDB {
     @BeforeMethod(groups = "fast")
     public void setUp() throws Exception {
         accountDao = new MockAccountDao(Mockito.mock(PersistentBus.class), clock);
-        accountUserApi = new DefaultAccountUserApi(accountDao, nonEntityDao, controllerDispatcher, internalFactory);
+        accountUserApi = new DefaultAccountUserApi(immutableAccountInternalApi, accountDao, nonEntityDao, controllerDispatcher, internalFactory);
     }
 
     @Test(groups = "fast", description = "Test Account create API")
diff --git a/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java b/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
index 237b8f5..e8ea8f0 100644
--- a/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
+++ b/account/src/test/java/org/killbill/billing/account/dao/TestAccountDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -26,6 +26,7 @@ import org.joda.time.DateTimeZone;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.AccountTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.account.api.AccountData;
 import org.killbill.billing.account.api.AccountEmail;
@@ -42,6 +43,7 @@ import org.killbill.billing.util.audit.AuditLog;
 import org.killbill.billing.util.audit.ChangeType;
 import org.killbill.billing.util.audit.DefaultAccountAuditLogs;
 import org.killbill.billing.util.customfield.dao.CustomFieldModelDao;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.killbill.billing.util.dao.TableName;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.tag.DescriptiveTag;
@@ -211,6 +213,18 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     public void testUpdate() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
+        final AccountModelDao createdAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history1 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), createdAccount.getRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), createdAccount.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), createdAccount.getExternalKey());
+        Assert.assertEquals(history1.get(0).getEntity().getMigrated(), createdAccount.getMigrated());
+        Assert.assertEquals(history1.get(0).getEntity().getIsNotifiedForInvoices(), createdAccount.getIsNotifiedForInvoices());
+        Assert.assertEquals(history1.get(0).getEntity().getTimeZone(), createdAccount.getTimeZone());
+        Assert.assertEquals(history1.get(0).getEntity().getLocale(), createdAccount.getLocale());
 
         final AccountData accountData = new MockAccountBuilder(new DefaultAccount(account)).migrated(false)
                                                                                            .isNotifiedForInvoices(false)
@@ -222,6 +236,40 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
 
         final AccountModelDao retrievedAccount = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
         checkAccountsEqual(retrievedAccount, updatedAccount);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history2 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), retrievedAccount.getRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), retrievedAccount.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), retrievedAccount.getExternalKey());
+        Assert.assertEquals(history2.get(1).getEntity().getMigrated(), retrievedAccount.getMigrated());
+        Assert.assertEquals(history2.get(1).getEntity().getIsNotifiedForInvoices(), retrievedAccount.getIsNotifiedForInvoices());
+        Assert.assertEquals(history2.get(1).getEntity().getTimeZone(), retrievedAccount.getTimeZone());
+        Assert.assertEquals(history2.get(1).getEntity().getLocale(), retrievedAccount.getLocale());
+
+        final AccountData accountData2 = new MockAccountBuilder(new DefaultAccount(updatedAccount)).isNotifiedForInvoices(true)
+                                                                                                   .locale("en_US")
+                                                                                                   .build();
+        final AccountModelDao updatedAccount2 = new AccountModelDao(account.getId(), accountData2);
+        accountDao.update(updatedAccount2, internalCallContext);
+
+        final AccountModelDao retrievedAccount2 = accountDao.getAccountByKey(account.getExternalKey(), internalCallContext);
+        checkAccountsEqual(retrievedAccount2, updatedAccount2);
+
+        final List<EntityHistoryModelDao<AccountModelDao, Account>> history3 = getAccountHistory(createdAccount.getRecordId());
+        Assert.assertEquals(history3.size(), 3);
+        Assert.assertEquals(history3.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history3.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getEntity().getAccountRecordId(), retrievedAccount2.getRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getTenantRecordId(), retrievedAccount2.getTenantRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getExternalKey(), retrievedAccount2.getExternalKey());
+        Assert.assertEquals(history3.get(2).getEntity().getMigrated(), retrievedAccount2.getMigrated());
+        Assert.assertEquals(history3.get(2).getEntity().getIsNotifiedForInvoices(), retrievedAccount2.getIsNotifiedForInvoices());
+        Assert.assertEquals(history3.get(2).getEntity().getTimeZone(), retrievedAccount2.getTimeZone());
+        Assert.assertEquals(history3.get(2).getEntity().getLocale(), retrievedAccount2.getLocale());
     }
 
     @Test(groups = "slow", description = "Test Account DAO: payment method update")
@@ -351,4 +399,10 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(auditLogsForAccountEmail2.size(), 1);
         Assert.assertEquals(auditLogsForAccountEmail2.get(0).getChangeType(), ChangeType.INSERT);
     }
+
+    private List<EntityHistoryModelDao<AccountModelDao, Account>> getAccountHistory(final Long accountRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final AccountSqlDao accountSqlDao = dbi.onDemand(AccountSqlDao.class);
+        return accountSqlDao.getHistoryForTargetRecordId(accountRecordId, internalCallContext);
+    }
 }

api/pom.xml 2(+1 -1)

diff --git a/api/pom.xml b/api/pom.xml
index c8dbd0c..9c71a87 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-internal-api</artifactId>
diff --git a/api/src/main/java/org/killbill/billing/entity/EntityBase.java b/api/src/main/java/org/killbill/billing/entity/EntityBase.java
index 0c02112..465debe 100644
--- a/api/src/main/java/org/killbill/billing/entity/EntityBase.java
+++ b/api/src/main/java/org/killbill/billing/entity/EntityBase.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -19,10 +21,10 @@ package org.killbill.billing.entity;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
-
 import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.entity.Entity;
 
+// Note: not suitable for Serializable Entity classes (e.g. DefaultTenant)
 public abstract class EntityBase implements Entity {
 
     protected UUID id;
@@ -45,7 +47,7 @@ public abstract class EntityBase implements Entity {
         this.updatedDate = updatedDate;
     }
 
-    public EntityBase(final EntityBase target) {
+    public EntityBase(final Entity target) {
         this.id = target.getId();
         this.createdDate = target.getCreatedDate();
         this.updatedDate = target.getUpdatedDate();

beatrix/pom.xml 2(+1 -1)

diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index 3b45ccc..cfdbaaa 100644
--- a/beatrix/pom.xml
+++ b/beatrix/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index 54788f8..3d4ea4a 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -270,13 +270,20 @@ public class TestSubscription extends TestIntegrationBase {
         invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
     }
 
-    @Test(groups = "slow", expectedExceptions = EntitlementApiException.class, expectedExceptionsMessageRegExp = "Missing Base Subscription.")
+    @Test(groups = "slow")
     public void testCreateMultipleSubscriptionsWithoutBase() throws Exception {
         final LocalDate initialDate = new LocalDate(2015, 10, 1);
         clock.setDay(initialDate);
 
         final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
 
+        final String externalKeyB = "baseExternalKeyBBB";
+
+        final DefaultEntitlement bpEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), externalKeyB, "Shotgun", ProductCategory.BASE, BillingPeriod.ANNUAL, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+        assertNotNull(bpEntitlement);
+        assertEquals(invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext).size(), 1);
+
+
         final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
         final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
         final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
@@ -291,7 +298,6 @@ public class TestSubscription extends TestIntegrationBase {
         specifierListA.add(addOnEntitlementSpecifier1);
         specifierListA.add(addOnEntitlementSpecifier2);
 
-        final String externalKeyB = "baseExternalKeyBBB";
         final List<EntitlementSpecifier> specifierListB = new ArrayList<EntitlementSpecifier>();
         specifierListB.add(addOnEntitlementSpecifier1);
         specifierListB.add(addOnEntitlementSpecifier2);
@@ -302,7 +308,22 @@ public class TestSubscription extends TestIntegrationBase {
         entitlementWithAddOnsSpecifierList.add(cartSpecifierA);
         entitlementWithAddOnsSpecifierList.add(cartSpecifierB);
 
-        entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), entitlementWithAddOnsSpecifierList, ImmutableList.<PluginProperty>of(), callContext);
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK,
+                                      NextEvent.CREATE, NextEvent.BLOCK,
+                                      NextEvent.CREATE, NextEvent.BLOCK,
+                                      NextEvent.CREATE, NextEvent.BLOCK,
+                                      NextEvent.CREATE, NextEvent.BLOCK,
+                                      NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE,
+                                      NextEvent.NULL_INVOICE, NextEvent.NULL_INVOICE,
+                                      NextEvent.INVOICE,
+                                      NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT
+                                     );
+
+        final List<Entitlement> entitlements = entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), entitlementWithAddOnsSpecifierList, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        Assert.assertEquals(entitlements.size(), 5);
+
     }
 
     @Test(groups = "slow", expectedExceptions = EntitlementApiException.class,
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java b/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java
index 1b0c6c9..d46e1d5 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/util/SubscriptionChecker.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -22,6 +24,7 @@ import java.util.UUID;
 import javax.inject.Inject;
 
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -46,14 +49,14 @@ public class SubscriptionChecker {
     private final SubscriptionBaseInternalApi subscriptionApi;
     private final AuditChecker auditChecker;
     private final NonEntityDao nonEntityDao;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
+    private final CacheController<String, UUID> objectIdCacheController;
 
     @Inject
     public SubscriptionChecker(final SubscriptionBaseInternalApi subscriptionApi, final AuditChecker auditChecker, final NonEntityDao nonEntityDao, final CacheControllerDispatcher cacheControllerDispatcher) {
         this.subscriptionApi = subscriptionApi;
         this.auditChecker = auditChecker;
         this.nonEntityDao = nonEntityDao;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
+        objectIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
     }
 
     public SubscriptionBaseBundle checkBundleNoAudits(final UUID bundleId, final UUID expectedAccountId, final String expectedKey, final InternalTenantContext context) throws SubscriptionBaseApiException {
@@ -65,7 +68,7 @@ public class SubscriptionChecker {
     }
 
     public SubscriptionBase checkSubscriptionCreated(final UUID subscriptionId, final InternalCallContext context) throws SubscriptionBaseApiException {
-        final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+        final UUID tenantId = nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, objectIdCacheController);
         final CallContext callContext = context.toCallContext(tenantId);
 
         final SubscriptionBase subscription = subscriptionApi.getSubscriptionFromId(subscriptionId, context);

catalog/pom.xml 2(+1 -1)

diff --git a/catalog/pom.xml b/catalog/pom.xml
index 77babaa..663881d 100644
--- a/catalog/pom.xml
+++ b/catalog/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-catalog</artifactId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
index 6a9c625..4d33096 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheCatalogCache.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -28,6 +28,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.StandaloneCatalog;
 import org.killbill.billing.catalog.StandaloneCatalogWithPriceOverride;
 import org.killbill.billing.catalog.VersionedCatalog;
+import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.catalog.io.VersionedCatalogLoader;
 import org.killbill.billing.catalog.override.PriceOverride;
@@ -53,7 +54,7 @@ public class EhCacheCatalogCache implements CatalogCache {
 
     private final Logger logger = LoggerFactory.getLogger(EhCacheCatalogCache.class);
 
-    private final CacheController cacheController;
+    private final CacheController<Long, Catalog> cacheController;
     private final VersionedCatalogLoader loader;
     private final CacheLoaderArgument cacheLoaderArgumentWithTemplateFiltering;
     private final CacheLoaderArgument cacheLoaderArgument;
@@ -97,7 +98,7 @@ public class EhCacheCatalogCache implements CatalogCache {
             return pluginVersionedCatalog;
         }
 
-        if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId())) {
             return useDefaultCatalog ? defaultCatalog : null;
         }
         // The cache loader might choke on some bad xml -- unlikely since we check its validity prior storing it,
@@ -113,7 +114,7 @@ public class EhCacheCatalogCache implements CatalogCache {
                     final StandaloneCatalogWithPriceOverride curWithOverride = new StandaloneCatalogWithPriceOverride(cur, priceOverride, tenantContext.getTenantRecordId(), internalCallContextFactory);
                     tenantCatalog.add(curWithOverride);
                 }
-                cacheController.add(tenantContext.getTenantRecordId(), tenantCatalog);
+                cacheController.putIfAbsent(tenantContext.getTenantRecordId(), tenantCatalog);
             }
             return tenantCatalog;
         } catch (final IllegalStateException e) {
@@ -123,7 +124,7 @@ public class EhCacheCatalogCache implements CatalogCache {
 
     @Override
     public void clearCatalog(final InternalTenantContext tenantContext) {
-        if (tenantContext.getTenantRecordId() != InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (!InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId())) {
             cacheController.remove(tenantContext.getTenantRecordId());
         }
     }
@@ -174,7 +175,7 @@ public class EhCacheCatalogCache implements CatalogCache {
     private CacheLoaderArgument initializeCacheLoaderArgument(final boolean filterTemplateCatalog) {
         final LoaderCallback loaderCallback = new LoaderCallback() {
             @Override
-            public Object loadCatalog(final List<String> catalogXMLs, final Long tenantRecordId) throws CatalogApiException {
+            public Catalog loadCatalog(final List<String> catalogXMLs, final Long tenantRecordId) throws CatalogApiException {
                 return loader.load(catalogXMLs, filterTemplateCatalog, tenantRecordId);
             }
         };
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
index 897d725..66e6a7d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/caching/EhCacheOverriddenPlanCache.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -61,7 +61,7 @@ import com.google.common.collect.Iterables;
 
 public class EhCacheOverriddenPlanCache implements OverriddenPlanCache {
 
-    private final CacheController cacheController;
+    private final CacheController<String, Plan> cacheController;
     private final LoaderCallback loaderCallback;
     private final CatalogOverrideDao overrideDao;
 
@@ -71,7 +71,7 @@ public class EhCacheOverriddenPlanCache implements OverriddenPlanCache {
         this.cacheController = cacheControllerDispatcher.getCacheController(CacheType.OVERRIDDEN_PLAN);
         this.loaderCallback = new LoaderCallback() {
             @Override
-            public Object loadPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException {
+            public Plan loadPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException {
                 return loadOverriddenPlan(planName, catalog, context);
             }
         };
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
index e4c347b..8e62c6c 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
@@ -82,7 +82,7 @@ public class DefaultBlock extends ValidatingConfig<StandaloneCatalog> implements
 
     @Override
     public Double getMinTopUpCredit() throws CatalogApiException {
-        if (minTopUpCredit != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE && type != BlockType.TOP_UP) {
+        if (!CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(minTopUpCredit) && type != BlockType.TOP_UP) {
             throw new CatalogApiException(ErrorCode.CAT_NOT_TOP_UP_BLOCK, phase.getName());
         }
         return minTopUpCredit;
@@ -95,7 +95,7 @@ public class DefaultBlock extends ValidatingConfig<StandaloneCatalog> implements
             throw new IllegalStateException("type should have been automatically been initialized with VANILLA ");
         }
 
-        if (type == BlockType.TOP_UP && minTopUpCredit == CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE) {
+        if (type == BlockType.TOP_UP && CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(minTopUpCredit)) {
             errors.add(new ValidationError(String.format("TOP_UP block needs to define minTopUpCredit for phase %s",
                                                          phase.getName()), catalog.getCatalogURI(), DefaultUsage.class, ""));
         }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
index f993a03..7af3daa 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
@@ -113,10 +113,10 @@ public class DefaultDuration extends ValidatingConfig<StandaloneCatalog> impleme
         }
 
         //Validation: TimeUnit UNLIMITED if number == -1
-        if ((unit == TimeUnit.UNLIMITED && number != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE)) {
+        if ((unit == TimeUnit.UNLIMITED && !CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE.equals(number))) {
             errors.add(new ValidationError("Duration can only have 'UNLIMITED' unit if the number is omitted",
                                            catalog.getCatalogURI(), DefaultDuration.class, ""));
-        } else if ((unit != TimeUnit.UNLIMITED) && number == CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE) {
+        } else if ((unit != TimeUnit.UNLIMITED) && CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE.equals(number)) {
             errors.add(new ValidationError("Finite Duration must have a well defined length",
                                            catalog.getCatalogURI(), DefaultDuration.class, ""));
         }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
index 10b11af..084b644 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
@@ -69,8 +69,8 @@ public class DefaultLimit extends ValidatingConfig<StandaloneCatalog> implements
 
     @Override
     public ValidationErrors validate(StandaloneCatalog root, ValidationErrors errors) {
-        if (max != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE &&
-                   min != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE &&
+        if (!CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(max) &&
+            !CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(min) &&
                    max.doubleValue() < min.doubleValue()) {
             errors.add(new ValidationError("max must be greater than min", root.getCatalogURI(), Limit.class, ""));
         }
@@ -86,12 +86,12 @@ public class DefaultLimit extends ValidatingConfig<StandaloneCatalog> implements
 
     @Override
     public boolean compliesWith(double value) {
-        if (max != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE) {
+        if (!CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(max)) {
             if (value > max.doubleValue()) {
                 return false;
             }
         }
-        if (min != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE) {
+        if (!CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_DOUBLE_FIELD_VALUE.equals(min)) {
             if (value < min.doubleValue()) {
                 return false;
             }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
index 0d1757f..f9aa7e4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,6 +18,10 @@
 
 package org.killbill.billing.catalog;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -46,12 +50,17 @@ import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.Recurring;
 import org.killbill.billing.catalog.api.TimeUnit;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 import org.killbill.xmlloader.ValidatingConfig;
 import org.killbill.xmlloader.ValidationError;
 import org.killbill.xmlloader.ValidationErrors;
 
 @XmlAccessorType(XmlAccessType.NONE)
-public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements Plan {
+public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements Plan, Externalizable {
+
+    private static final long serialVersionUID = -4159932819592790086L;
 
     @XmlAttribute(required = true)
     @XmlID
@@ -80,6 +89,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
 
     private String priceListName;
 
+    // For deserialization
     public DefaultPlan() {
         initialPhases = new DefaultPlanPhase[0];
     }
@@ -102,31 +112,61 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
         return effectiveDateForExistingSubscriptions;
     }
 
+    public void setEffectiveDateForExistingSubscriptions(
+            final Date effectiveDateForExistingSubscriptions) {
+        this.effectiveDateForExistingSubscriptions = effectiveDateForExistingSubscriptions;
+    }
+
     @Override
     public DefaultPlanPhase[] getInitialPhases() {
         return initialPhases;
     }
 
+    public DefaultPlan setInitialPhases(final DefaultPlanPhase[] phases) {
+        this.initialPhases = phases;
+        return this;
+    }
+
     @Override
     public Product getProduct() {
         return product;
     }
 
+    public DefaultPlan setProduct(final Product product) {
+        this.product = (DefaultProduct) product;
+        return this;
+    }
+
     @Override
     public String getPriceListName() {
         return priceListName;
     }
 
+    public DefaultPlan setPriceListName(final String priceListName) {
+        this.priceListName = priceListName;
+        return this;
+    }
+
     @Override
     public String getName() {
         return name;
     }
 
+    public DefaultPlan setName(final String name) {
+        this.name = name;
+        return this;
+    }
+
     @Override
     public DefaultPlanPhase getFinalPhase() {
         return finalPhase;
     }
 
+    public DefaultPlan setFinalPhase(final DefaultPlanPhase finalPhase) {
+        this.finalPhase = finalPhase;
+        return this;
+    }
+
     @Override
     public PlanPhase[] getAllPhases() {
         final int length = initialPhases.length + 1;
@@ -162,9 +202,11 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
         return plansAllowedInBundle;
     }
 
-    /* (non-Javadoc)
-      * @see org.killbill.billing.catalog.IPlan#getPhaseIterator()
-      */
+    public DefaultPlan setPlansAllowedInBundle(final Integer plansAllowedInBundle) {
+        this.plansAllowedInBundle = plansAllowedInBundle;
+        return this;
+    }
+
     @Override
     public Iterator<PlanPhase> getInitialPhaseIterator() {
         final Collection<PlanPhase> list = new ArrayList<PlanPhase>();
@@ -185,7 +227,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
             p.setPlan(this);
             p.initialize(catalog, sourceURI);
         }
-        this.priceListName = this.priceListName  != null ? this.priceListName : findPriceListForPlan(catalog);
+        this.priceListName = this.priceListName != null ? this.priceListName : findPriceListForPlan(catalog);
     }
 
     @Override
@@ -202,7 +244,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
             errors.add(new ValidationError(String.format("Invalid product for plan '%s'", name), catalog.getCatalogURI(), DefaultPlan.class, ""));
         }
 
-        for (DefaultPlanPhase cur  : initialPhases) {
+        for (final DefaultPlanPhase cur : initialPhases) {
             cur.validate(catalog, errors);
             if (cur.getPhaseType() == PhaseType.EVERGREEN || cur.getPhaseType() == PhaseType.FIXEDTERM) {
                 errors.add(new ValidationError(String.format("Initial Phase %s of plan %s cannot be of type %s",
@@ -225,41 +267,6 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
         return errors;
     }
 
-    public void setEffectiveDateForExistingSubscriptions(
-            final Date effectiveDateForExistingSubscriptions) {
-        this.effectiveDateForExistingSubscriptions = effectiveDateForExistingSubscriptions;
-    }
-
-    public DefaultPlan setName(final String name) {
-        this.name = name;
-        return this;
-    }
-
-    public DefaultPlan setFinalPhase(final DefaultPlanPhase finalPhase) {
-        this.finalPhase = finalPhase;
-        return this;
-    }
-
-    public DefaultPlan setProduct(final Product product) {
-        this.product = (DefaultProduct) product;
-        return this;
-    }
-
-    public DefaultPlan setPriceListName(final String priceListName) {
-        this.priceListName = priceListName;
-        return this;
-    }
-
-    public DefaultPlan setInitialPhases(final DefaultPlanPhase[] phases) {
-        this.initialPhases = phases;
-        return this;
-    }
-
-    public DefaultPlan setPlansAllowedInBundle(final Integer plansAllowedInBundle) {
-        this.plansAllowedInBundle = plansAllowedInBundle;
-        return this;
-    }
-
     @Override
     public DateTime dateOfFirstRecurringNonZeroCharge(final DateTime subscriptionStartDate, final PhaseType initialPhaseType) {
         DateTime result = subscriptionStartDate;
@@ -345,4 +352,14 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements 
         }
         throw new IllegalStateException("Cannot extract pricelist for plan " + name);
     }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index 8d31aff..e52d6a8 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,6 +18,10 @@
 
 package org.killbill.billing.catalog;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -58,6 +62,9 @@ import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.StaticCatalog;
 import org.killbill.billing.catalog.api.Unit;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 import org.killbill.clock.Clock;
 import org.killbill.xmlloader.ValidatingConfig;
 import org.killbill.xmlloader.ValidationError;
@@ -65,7 +72,9 @@ import org.killbill.xmlloader.ValidationErrors;
 
 @XmlRootElement(name = "catalogs")
 @XmlAccessorType(XmlAccessType.NONE)
-public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> implements Catalog, StaticCatalog {
+public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> implements Catalog, StaticCatalog, Externalizable {
+
+    private static final long serialVersionUID = 3181874902672322725L;
 
     private final Clock clock;
 
@@ -553,4 +562,14 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
     public boolean compliesWithLimits(final String phaseName, final String unit, final double value) throws CatalogApiException {
         return versionForDate(clock.getUTCNow()).compliesWithLimits(phaseName, unit, value);
     }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }

currency/pom.xml 2(+1 -1)

diff --git a/currency/pom.xml b/currency/pom.xml
index 26b69c6..6592d18 100644
--- a/currency/pom.xml
+++ b/currency/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-currency</artifactId>
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 2c34d9c..7c236e7 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index 7ea0b3b..82e0c05 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -155,9 +155,6 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
             public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
                 final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
                 try {
-                    if (entitlementUtils.getFirstActiveSubscriptionIdForKeyOrNull(externalKey, contextWithValidAccountRecordId) != null) {
-                        throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, externalKey));
-                    }
 
                     final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.createBundleForAccount(accountId, externalKey, contextWithValidAccountRecordId);
 
@@ -222,12 +219,6 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                 final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
 
                 try {
-                    // First verify bundleKey
-                    for (final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier : baseEntitlementWithAddOnsSpecifiers) {
-                        if (entitlementUtils.getFirstActiveSubscriptionIdForKeyOrNull(baseEntitlementWithAddOnsSpecifier.getExternalKey(), contextWithValidAccountRecordId) != null) {
-                            throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, baseEntitlementWithAddOnsSpecifier.getExternalKey()));
-                        }
-                    }
 
                     final List<SubscriptionBaseWithAddOns> subscriptionsWithAddOns = subscriptionBaseInternalApi.createBaseSubscriptionsWithAddOns(accountId, baseEntitlementWithAddOnsSpecifiers, contextWithValidAccountRecordId);
                     final Map<BlockingState, UUID> blockingStateMap = new HashMap<BlockingState, UUID>();
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
index db2577e..29c193c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/dao/DefaultBlockingStateDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -36,7 +36,6 @@ import org.killbill.billing.ObjectType;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.entitlement.DefaultEntitlementService;
-import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.api.BlockingApiException;
 import org.killbill.billing.entitlement.api.BlockingState;
 import org.killbill.billing.entitlement.api.BlockingStateType;
@@ -46,6 +45,7 @@ import org.killbill.billing.entitlement.block.BlockingChecker.BlockingAggregator
 import org.killbill.billing.entitlement.block.StatelessBlockingChecker;
 import org.killbill.billing.entitlement.engine.core.BlockingTransitionNotificationKey;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.NonEntityDao;
@@ -107,7 +107,7 @@ public class DefaultBlockingStateDao extends EntityDaoBase<BlockingStateModelDao
     private final Clock clock;
     private final NotificationQueueService notificationQueueService;
     private final PersistentBus eventBus;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
+    private final CacheController<String, UUID> objectIdCacheController;
     private final NonEntityDao nonEntityDao;
 
     private final StatelessBlockingChecker statelessBlockingChecker = new StatelessBlockingChecker();
@@ -118,7 +118,7 @@ public class DefaultBlockingStateDao extends EntityDaoBase<BlockingStateModelDao
         this.clock = clock;
         this.notificationQueueService = notificationQueueService;
         this.eventBus = eventBus;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
+        this.objectIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
         this.nonEntityDao = nonEntityDao;
     }
 
@@ -230,7 +230,7 @@ public class DefaultBlockingStateDao extends EntityDaoBase<BlockingStateModelDao
                     boolean inserted = false;
                     // Create the state, if needed
                     if (!blockingStatesToRemove.contains(newBlockingStateModelDao.getId())) {
-                        sqlDao.create(newBlockingStateModelDao, context);
+                        createAndRefresh(sqlDao, newBlockingStateModelDao, context);
                         inserted = true;
                     }
 
@@ -260,12 +260,12 @@ public class DefaultBlockingStateDao extends EntityDaoBase<BlockingStateModelDao
         final List<BlockingState> bundleBlockingStates;
         final List<BlockingState> subscriptionBlockingStates;
         if (type == BlockingStateType.SUBSCRIPTION) {
-            final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), handle);
+            final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, objectIdCacheController, handle);
             accountBlockingStates = getBlockingState(sqlDao, accountId, BlockingStateType.ACCOUNT, upToDate, context);
             bundleBlockingStates = getBlockingState(sqlDao, bundleId, BlockingStateType.SUBSCRIPTION_BUNDLE, upToDate, context);
             subscriptionBlockingStates = getBlockingState(sqlDao, blockableId, BlockingStateType.SUBSCRIPTION, upToDate, context);
         } else if (type == BlockingStateType.SUBSCRIPTION_BUNDLE) {
-            final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), handle);
+            final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, objectIdCacheController, handle);
             accountBlockingStates = getBlockingState(sqlDao, accountId, BlockingStateType.ACCOUNT, upToDate, context);
             bundleBlockingStates = getBlockingState(sqlDao, blockableId, BlockingStateType.SUBSCRIPTION_BUNDLE, upToDate, context);
             subscriptionBlockingStates = ImmutableList.<BlockingState>of();
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
index 0d5f109..5eb96de 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -724,4 +724,92 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
         assertEquals(entitlement.getEffectiveStartDate(), initialDate);
     }
 
+
+    @Test(groups = "slow")
+    public void testCreateBaseSubscriptionsWithAddOns() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = createAccount(getAccountData(7));
+
+        final String bundleKey2 = "bundleKey2";
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Shotgun",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CREATE);
+        entitlementApi.createBaseEntitlement(account.getId(), spec, bundleKey2, null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+
+        // First bundle of EntitlementSpecifier will specify all new subscription
+        final String bundleKey1 = "bundleKey1";
+        final EntitlementSpecifier spec11 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Shotgun",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final EntitlementSpecifier spec12 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Telescopic-Scope",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final List<EntitlementSpecifier> specs1 = ImmutableList.of(spec11, spec12);
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier1 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey1, specs1, null, null, false);
+
+
+        // Second bundle of EntitlementSpecifier will specify the previously created 'existingEntitlement'
+        final EntitlementSpecifier spec22 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Telescopic-Scope",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final List<EntitlementSpecifier> specs2 = ImmutableList.of(spec22);
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier2 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey2, specs2, null, null, false);
+
+        final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers = ImmutableList.of(baseEntitlementWithAddOnsSpecifier1, baseEntitlementWithAddOnsSpecifier2);
+
+
+        // We expect 3 {BLOCK, CREATE} events for the 3 subscriptions created,.
+        testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE);
+        final List<Entitlement> entitlements = entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), baseEntitlementWithAddOnsSpecifiers, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        // Retun only the created subscriptions
+        Assert.assertEquals(entitlements.size(), 3);
+
+        final List<Entitlement> entitlementsForBundle1 = entitlementApi.getAllEntitlementsForAccountIdAndExternalKey(account.getId(), bundleKey1, callContext);
+        Assert.assertEquals(entitlementsForBundle1.size(), 2);
+
+        // And yet we do have both the BASE and ADD_ON for bundleKey2
+        final List<Entitlement> entitlementsForBundle2 = entitlementApi.getAllEntitlementsForAccountIdAndExternalKey(account.getId(), bundleKey2, callContext);
+        Assert.assertEquals(entitlementsForBundle2.size(), 2);
+
+    }
+
+
+    @Test(groups = "slow", expectedExceptions = EntitlementApiException.class)
+    public void testCreateBaseSubscriptionsWithAddOnsMissingBase() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = createAccount(getAccountData(7));
+
+        final String bundleKey2 = "bundleKey2";
+
+        final EntitlementSpecifier spec22 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Telescopic-Scope",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final List<EntitlementSpecifier> specs2 = ImmutableList.of(spec22);
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier2 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey2, specs2, null, null, false);
+
+        final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers = ImmutableList.of(baseEntitlementWithAddOnsSpecifier2);
+
+        entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), baseEntitlementWithAddOnsSpecifiers, ImmutableList.<PluginProperty>of(), callContext);
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseSubscriptionsWithAddOnsBadOrdering() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = createAccount(getAccountData(7));
+
+        final String bundleKey1 = "bundleKey1";
+        final EntitlementSpecifier spec11 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Telescopic-Scope",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final EntitlementSpecifier spec12 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Shotgun",  BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null), null);
+        final List<EntitlementSpecifier> specs1 = ImmutableList.of(spec11, spec12);
+        final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier1 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey1, specs1, null, null, false);
+
+        final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers = ImmutableList.of(baseEntitlementWithAddOnsSpecifier1);
+
+
+        testListener.pushExpectedEvents(NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE);
+        entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), baseEntitlementWithAddOnsSpecifiers, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+    }
 }

invoice/pom.xml 2(+1 -1)

diff --git a/invoice/pom.xml b/invoice/pom.xml
index 74f81c4..dd27586 100644
--- a/invoice/pom.xml
+++ b/invoice/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 4976f49..0afa55c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -59,6 +59,7 @@ import org.killbill.billing.invoice.notification.ParentInvoiceCommitmentPoster;
 import org.killbill.billing.tag.TagInternalApi;
 import org.killbill.billing.util.UUIDs;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.config.definition.InvoiceConfig;
@@ -115,7 +116,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
     private final CBADao cbaDao;
     private final InvoiceConfig invoiceConfig;
     private final Clock clock;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
+    private final CacheController<String, UUID> objectIdCacheController;
     private final NonEntityDao nonEntityDao;
     private final ParentInvoiceCommitmentPoster parentInvoiceCommitmentPoster;
     private final TagInternalApi tagInternalApi;
@@ -142,7 +143,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         this.invoiceDaoHelper = invoiceDaoHelper;
         this.cbaDao = cbaDao;
         this.clock = clock;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
+        this.objectIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
         this.nonEntityDao = nonEntityDao;
         this.parentInvoiceCommitmentPoster = parentInvoiceCommitmentPoster;
     }
@@ -325,7 +326,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                         // We only want to insert that invoice if there are real invoiceItems associated to it -- if not, this is just
                         // a shell invoice and we only need to insert the invoiceItems -- for the already existing invoices
                         if (isRealInvoice) {
-                            invoiceSqlDao.create(invoiceModelDao, context);
+                            createAndRefresh(invoiceSqlDao, invoiceModelDao, context);
                             createdInvoiceIds.add(invoiceModelDao.getId());
                         }
                     }
@@ -333,8 +334,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                     // Create the invoice items if needed (note: they may not necessarily belong to that invoice)
                     for (final InvoiceItemModelDao invoiceItemModelDao : invoiceModelDao.getInvoiceItems()) {
                         if (transInvoiceItemSqlDao.getById(invoiceItemModelDao.getId().toString(), context) == null) {
-                            createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context);
-                            createdInvoiceItems.add(transInvoiceItemSqlDao.getById(invoiceItemModelDao.getId().toString(), context));
+                            createdInvoiceItems.add(createInvoiceItemFromTransaction(transInvoiceItemSqlDao, invoiceItemModelDao, context));
 
                             adjustedInvoiceIds.add(invoiceItemModelDao.getInvoiceId());
                         }
@@ -550,14 +550,13 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                                                                                  payment.getInvoiceId(), paymentId,
                                                                                  context.getCreatedDate(), requestedPositiveAmount.negate(),
                                                                                  payment.getCurrency(), payment.getProcessedCurrency(), transactionExternalKey, payment.getId(), true);
-                transactional.create(refund, context);
+                createAndRefresh(transactional, refund, context);
 
                 // Retrieve invoice after the Refund
                 final InvoiceModelDao invoice = transInvoiceDao.getById(payment.getInvoiceId().toString(), context);
                 Preconditions.checkState(invoice != null, "Invoice shouldn't be null for payment " + payment.getId());
                 invoiceDaoHelper.populateChildren(invoice, invoicesTags, entitySqlDaoWrapperFactory, context);
 
-                final BigDecimal invoiceBalanceAfterRefund = InvoiceModelDaoHelper.getBalance(invoice);
                 final InvoiceItemSqlDao transInvoiceItemDao = entitySqlDaoWrapperFactory.become(InvoiceItemSqlDao.class);
 
                 // At this point, we created the refund which made the invoice balance positive and applied any existing
@@ -630,7 +629,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                                                                                      payment.getInvoiceId(), payment.getPaymentId(), context.getCreatedDate(),
                                                                                      requestedChargedBackAmount.negate(), payment.getCurrency(), payment.getProcessedCurrency(),
                                                                                      chargebackTransactionExternalKey, payment.getId(), true);
-                transactional.create(chargeBack, context);
+                createAndRefresh(transactional, chargeBack, context);
 
                 // Notify the bus since the balance of the invoice changed
                 final UUID accountId = transactional.getAccountIdFromInvoicePaymentId(chargeBack.getId().toString(), context);
@@ -816,7 +815,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                     }).orNull();
 
                     if (existingAttempt == null) {
-                        transactional.create(invoicePayment, context);
+                        createAndRefresh(transactional, invoicePayment, context);
                     } else {
                         transactional.updateAttempt(existingAttempt.getRecordId(),
                                                     invoicePayment.getPaymentId().toString(),
@@ -832,7 +831,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
                 }
 
                 if (completion) {
-                    final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID), entitySqlDaoWrapperFactory.getHandle());
+                    final UUID accountId = nonEntityDao.retrieveIdFromObjectInTransaction(context.getAccountRecordId(), ObjectType.ACCOUNT, objectIdCacheController, entitySqlDaoWrapperFactory.getHandle());
                     notifyBusOfInvoicePayment(entitySqlDaoWrapperFactory, invoicePayment, accountId, context.getUserToken(), context);
                 }
                 return null;
@@ -1032,7 +1031,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
         }
     }
 
-    private void createInvoiceItemFromTransaction(final InvoiceItemSqlDao invoiceItemSqlDao, final InvoiceItemModelDao invoiceItemModelDao, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
+    private InvoiceItemModelDao createInvoiceItemFromTransaction(final InvoiceItemSqlDao invoiceItemSqlDao, final InvoiceItemModelDao invoiceItemModelDao, final InternalCallContext context) throws EntityPersistenceException, InvoiceApiException {
         // There is no efficient way to retrieve an invoice item given an ID today (and invoice plugins can put item adjustments
         // on a different invoice than the original item), so it's easier to do the check in the DAO rather than in the API layer
         // See also https://github.com/killbill/killbill/issues/7
@@ -1040,7 +1039,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
             validateInvoiceItemToBeAdjusted(invoiceItemSqlDao, invoiceItemModelDao, context);
         }
 
-        invoiceItemSqlDao.create(invoiceItemModelDao, context);
+        return createAndRefresh(invoiceItemSqlDao, invoiceItemModelDao, context);
     }
 
     private void validateInvoiceItemToBeAdjusted(final InvoiceItemSqlDao invoiceItemSqlDao, final InvoiceItemModelDao invoiceItemModelDao, final InternalCallContext context) throws InvoiceApiException {
@@ -1114,7 +1113,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final InvoiceParentChildrenSqlDao transactional = entitySqlDaoWrapperFactory.become(InvoiceParentChildrenSqlDao.class);
-                transactional.create(invoiceRelation, context);
+                createAndRefresh(transactional, invoiceRelation, context);
                 return null;
             }
         });
@@ -1230,14 +1229,14 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
 
                 // save invoices and invoice items
                 final InvoiceModelDao childInvoice = new InvoiceModelDao(invoiceForExternalCharge);
-                invoiceSqlDao.create(childInvoice, childAccountContext);
+                createAndRefresh(invoiceSqlDao, childInvoice, childAccountContext);
                 final InvoiceItemModelDao childExternalChargeItem = new InvoiceItemModelDao(externalChargeItem);
                 createInvoiceItemFromTransaction(transInvoiceItemSqlDao, childExternalChargeItem, childAccountContext);
                 // Keep invoice up-to-date for CBA below
                 childInvoice.addInvoiceItem(childExternalChargeItem);
 
                 final InvoiceModelDao parentInvoice = new InvoiceModelDao(invoiceForCredit);
-                invoiceSqlDao.create(parentInvoice, parentAccountContext);
+                createAndRefresh(invoiceSqlDao, parentInvoice, parentAccountContext);
                 final InvoiceItemModelDao parentCreditItem = new InvoiceItemModelDao(creditItem);
                 createInvoiceItemFromTransaction(transInvoiceItemSqlDao, parentCreditItem, parentAccountContext);
                 // Keep invoice up-to-date for CBA below
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java
index f989471..b5bcb96 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java
@@ -51,7 +51,7 @@ public class DefaultResourceBundleFactory implements ResourceBundleFactory {
 
     @Override
     public ResourceBundle createBundle(final Locale locale, final String bundlePath, final ResourceBundleType type, final InternalTenantContext tenantContext) {
-        if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId())) {
             return getGlobalBundle(locale, bundlePath);
         }
         final String bundle = getTenantBundleForType(locale, type, tenantContext);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
index 3e0e0bc..16f391c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
@@ -101,7 +101,7 @@ public class HtmlInvoiceGenerator {
 
     private String getTemplateText(final Locale locale, final boolean manualPay, final InternalTenantContext context) throws IOException {
 
-        if (context.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(context.getTenantRecordId())) {
             return getDefaultTemplate(manualPay ? config.getManualPayTemplateName() : config.getTemplateName());
         }
         final String template = manualPay ?
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
index 6d083d1..51ddb3a 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/notification/TestNextBillingDateNotifier.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -20,14 +22,14 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 
 import org.joda.time.DateTime;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.Account;
 import org.killbill.billing.invoice.InvoiceTestSuiteWithEmbeddedDB;
 import org.killbill.billing.invoice.api.DefaultInvoiceService;
+import org.killbill.billing.subscription.api.SubscriptionBase;
 import org.killbill.notificationq.api.NotificationQueue;
-import org.killbill.clock.ClockMock;
+import org.testng.Assert;
+import org.testng.annotations.Test;
 
 import static com.jayway.awaitility.Awaitility.await;
 import static java.util.concurrent.TimeUnit.MINUTES;
@@ -36,19 +38,19 @@ public class TestNextBillingDateNotifier extends InvoiceTestSuiteWithEmbeddedDB 
 
     @Test(groups = "slow")
     public void testInvoiceNotifier() throws Exception {
+        final Account account = invoiceUtil.createAccount(callContext);
+        final Long accountRecordId = nonEntityDao.retrieveAccountRecordIdFromObject(account.getId(), ObjectType.ACCOUNT, null);
 
         final SubscriptionBase subscription = invoiceUtil.createSubscription();
         final UUID subscriptionId = subscription.getId();
         final DateTime now = clock.getUTCNow();
 
-
         final NotificationQueue nextBillingQueue = notificationQueueService.getNotificationQueue(DefaultInvoiceService.INVOICE_SERVICE_NAME, DefaultNextBillingDateNotifier.NEXT_BILLING_DATE_NOTIFIER_QUEUE);
 
-
-        nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId, now, Boolean.FALSE), internalCallContext.getUserToken(), internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+        nextBillingQueue.recordFutureNotification(now, new NextBillingDateNotificationKey(subscriptionId, now, Boolean.FALSE), internalCallContext.getUserToken(), accountRecordId, internalCallContext.getTenantRecordId());
 
         // Move time in the future after the notification effectiveDate
-        ((ClockMock) clock).setDeltaFromReality(3000);
+        clock.setDeltaFromReality(3000);
 
         await().atMost(1, MINUTES).until(new Callable<Boolean>() {
             @Override
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
index 7708eb6..06ee314 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestInvoiceHelper.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -52,8 +52,6 @@ import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.Usage;
 import org.killbill.billing.entitlement.api.SubscriptionEventType;
 import org.killbill.billing.entity.EntityPersistenceException;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications;
-import org.killbill.billing.invoice.InvoiceDispatcher.FutureAccountNotifications.SubscriptionNotification;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.invoice.api.DryRunType;
 import org.killbill.billing.invoice.api.Invoice;
@@ -95,7 +93,6 @@ import org.testng.Assert;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 
 public class TestInvoiceHelper {
 

jaxrs/pom.xml 6(+1 -5)

diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index b76c36c..fc79a8e 100644
--- a/jaxrs/pom.xml
+++ b/jaxrs/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-jaxrs</artifactId>
@@ -76,10 +76,6 @@
             <artifactId>joda-time</artifactId>
         </dependency>
         <dependency>
-            <groupId>net.sf.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-core</artifactId>
         </dependency>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
index 048a3c5..a190bd2 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/OverdueConditionJson.java
@@ -119,13 +119,13 @@ public class OverdueConditionJson {
         if (controlTagExclusion != that.controlTagExclusion) {
             return false;
         }
-        if (numberOfUnpaidInvoicesEqualsOrExceeds != that.numberOfUnpaidInvoicesEqualsOrExceeds) {
+        if (!numberOfUnpaidInvoicesEqualsOrExceeds.equals(that.numberOfUnpaidInvoicesEqualsOrExceeds)) {
             return false;
         }
         if (responseForLastFailedPayment != that.responseForLastFailedPayment) {
             return false;
         }
-        return totalUnpaidInvoiceBalanceEqualsOrExceeds == that.totalUnpaidInvoiceBalanceEqualsOrExceeds;
+        return totalUnpaidInvoiceBalanceEqualsOrExceeds.equals(that.totalUnpaidInvoiceBalanceEqualsOrExceeds);
 
     }
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
index 27168ba..e9af063 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
@@ -47,6 +47,8 @@ import org.joda.time.DateTimeZone;
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.ObjectType;
 import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.invoice.api.InvoiceApiException;
 import org.killbill.billing.invoice.api.InvoiceUserApi;
 import org.killbill.billing.jaxrs.json.AdminPaymentJson;
@@ -67,8 +69,11 @@ import org.killbill.billing.util.api.CustomFieldUserApi;
 import org.killbill.billing.util.api.RecordIdApi;
 import org.killbill.billing.util.api.TagUserApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.TenantContext;
+import org.killbill.billing.util.config.tenant.PerTenantConfig;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.tag.Tag;
 import org.killbill.billing.util.tag.dao.SystemTags;
@@ -82,6 +87,7 @@ import org.killbill.notificationq.api.NotificationQueue;
 import org.killbill.notificationq.api.NotificationQueueService;
 
 import com.fasterxml.jackson.core.JsonGenerator;
+import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -92,8 +98,6 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
@@ -107,7 +111,7 @@ public class AdminResource extends JaxRsResourceBase {
     private final AdminPaymentApi adminPaymentApi;
     private final InvoiceUserApi invoiceUserApi;
     private final TenantUserApi tenantApi;
-    private final CacheManager cacheManager;
+    private final CacheControllerDispatcher cacheControllerDispatcher;
     private final RecordIdApi recordIdApi;
     private final PersistentBus persistentBus;
     private final NotificationQueueService notificationQueueService;
@@ -121,7 +125,7 @@ public class AdminResource extends JaxRsResourceBase {
                          final PaymentApi paymentApi,
                          final AdminPaymentApi adminPaymentApi,
                          final InvoiceUserApi invoiceUserApi,
-                         final CacheManager cacheManager,
+                         final CacheControllerDispatcher cacheControllerDispatcher,
                          final TenantUserApi tenantApi,
                          final RecordIdApi recordIdApi,
                          final PersistentBus persistentBus,
@@ -133,7 +137,7 @@ public class AdminResource extends JaxRsResourceBase {
         this.invoiceUserApi = invoiceUserApi;
         this.tenantApi = tenantApi;
         this.recordIdApi = recordIdApi;
-        this.cacheManager = cacheManager;
+        this.cacheControllerDispatcher = cacheControllerDispatcher;
         this.persistentBus = persistentBus;
         this.notificationQueueService = notificationQueueService;
     }
@@ -322,17 +326,17 @@ public class AdminResource extends JaxRsResourceBase {
     public Response invalidatesCache(@QueryParam("cacheName") final String cacheName,
                                      @javax.ws.rs.core.Context final HttpServletRequest request) {
         if (null != cacheName && !cacheName.isEmpty()) {
-            final Ehcache cache = cacheManager.getEhcache(cacheName);
-            // check if cache is null
-            if (cache == null) {
+            // Clear given cache
+            final CacheType cacheType = CacheType.findByName(cacheName);
+            if (cacheType != null) {
+                cacheControllerDispatcher.getCacheController(cacheType).removeAll();
+            } else {
                 log.warn("Cache for specified cacheName='{}' does not exist or is not alive", cacheName);
                 return Response.status(Status.BAD_REQUEST).build();
             }
-            // Clear given cache
-            cache.removeAll();
         } else {
             // if not given a specific cacheName, clear all
-            cacheManager.clearAll();
+            cacheControllerDispatcher.clearAll();
         }
         return Response.status(Status.OK).build();
     }
@@ -342,20 +346,23 @@ public class AdminResource extends JaxRsResourceBase {
     @Produces(APPLICATION_JSON)
     @ApiOperation(value = "Invalidates Caches per account level")
     @ApiResponses(value = {})
-    public Response invalidatesCacheByAccount(@PathParam("accountId") final String accountId,
+    public Response invalidatesCacheByAccount(@PathParam("accountId") final String accountIdStr,
                                               @javax.ws.rs.core.Context final HttpServletRequest request) {
+        final TenantContext tenantContext = context.createContext(request);
+        final UUID accountId = UUID.fromString(accountIdStr);
+        final Long accountRecordId = recordIdApi.getRecordId(accountId, ObjectType.ACCOUNT, tenantContext);
 
-        // clear account-record-id cache by accountId
-        final Ehcache accountRecordIdCache = cacheManager.getEhcache(CacheType.ACCOUNT_RECORD_ID.getCacheName());
-        accountRecordIdCache.remove(accountId);
+        // clear account-record-id cache by accountId (note: String!)
+        final CacheController<String, Long> accountRecordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID);
+        accountRecordIdCacheController.remove(accountIdStr);
 
-        // clear account-immutable cache by accountId
-        final Ehcache accountImmutableCache = cacheManager.getEhcache(CacheType.ACCOUNT_IMMUTABLE.getCacheName());
-        accountImmutableCache.remove(UUID.fromString(accountId));
+        // clear account-immutable cache by account record id
+        final CacheController<Long, ImmutableAccountData> accountImmutableCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+        accountImmutableCacheController.remove(accountRecordId);
 
-        // clear account-bcd cache by accountId
-        final Ehcache accountBcdCache = cacheManager.getEhcache(CacheType.ACCOUNT_BCD.getCacheName());
-        accountBcdCache.remove(UUID.fromString(accountId));
+        // clear account-bcd cache by accountId (note: UUID!)
+        final CacheController<UUID, Integer> accountBCDCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_BCD);
+        accountBCDCacheController.remove(accountId);
 
         return Response.status(Status.OK).build();
     }
@@ -369,52 +376,51 @@ public class AdminResource extends JaxRsResourceBase {
                                              @javax.ws.rs.core.Context final HttpServletRequest request) throws TenantApiException {
 
         // creating Tenant Context from Request
-        TenantContext tenantContext = context.createContext(request);
+        final TenantContext tenantContext = context.createContext(request);
 
-        Tenant currentTenant = tenantApi.getTenantById(tenantContext.getTenantId());
+        final Tenant currentTenant = tenantApi.getTenantById(tenantContext.getTenantId());
 
         // getting Tenant Record Id
-        Long tenantRecordId = recordIdApi.getRecordId(tenantContext.getTenantId(), ObjectType.TENANT, tenantContext);
+        final Long tenantRecordId = recordIdApi.getRecordId(tenantContext.getTenantId(), ObjectType.TENANT, tenantContext);
+
+        final Function<String, Boolean> tenantKeysMatcher = new Function<String, Boolean>() {
+            @Override
+            public Boolean apply(@Nullable final String key) {
+                return key != null && key.endsWith("::" + tenantRecordId);
+            }
+        };
 
         // clear tenant-record-id cache by tenantId
-        final Ehcache tenantRecordIdCache = cacheManager.getEhcache(CacheType.TENANT_RECORD_ID.getCacheName());
-        tenantRecordIdCache.remove(currentTenant.getId().toString());
+        final CacheController<String, Long> tenantRecordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID);
+        tenantRecordIdCacheController.remove(currentTenant.getId().toString());
 
         // clear tenant-payment-state-machine-config cache by tenantRecordId
-        final Ehcache tenantPaymentStateMachineConfigCache = cacheManager.getEhcache(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG.getCacheName());
-        removeCacheByKey(tenantPaymentStateMachineConfigCache, tenantRecordId.toString());
+        final CacheController<String, Object> tenantPaymentStateMachineConfigCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG);
+        tenantPaymentStateMachineConfigCacheController.remove(tenantKeysMatcher);
 
         // clear tenant cache by tenantApiKey
-        final Ehcache tenantCache = cacheManager.getEhcache(CacheType.TENANT.getCacheName());
-        tenantCache.remove(currentTenant.getApiKey());
+        final CacheController<String, Tenant> tenantCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT);
+        tenantCacheController.remove(currentTenant.getApiKey());
 
         // clear tenant-kv cache by tenantRecordId
-        final Ehcache tenantKvCache = cacheManager.getEhcache(CacheType.TENANT_KV.getCacheName());
-        removeCacheByKey(tenantKvCache, tenantRecordId.toString());
+        final CacheController<String, String> tenantKVCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_KV);
+        tenantKVCacheController.remove(tenantKeysMatcher);
 
         // clear tenant-config cache by tenantRecordId
-        final Ehcache tenantConfigCache = cacheManager.getEhcache(CacheType.TENANT_CONFIG.getCacheName());
-        tenantConfigCache.remove(tenantRecordId);
+        final CacheController<Long, PerTenantConfig> tenantConfigCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_CONFIG);
+        tenantConfigCacheController.remove(tenantRecordId);
 
         // clear tenant-overdue-config cache by tenantRecordId
-        final Ehcache tenantOverdueConfigCache = cacheManager.getEhcache(CacheType.TENANT_OVERDUE_CONFIG.getCacheName());
-        tenantOverdueConfigCache.remove(tenantRecordId);
+        final CacheController<Long, Object> tenantOverdueConfigCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_OVERDUE_CONFIG);
+        tenantOverdueConfigCacheController.remove(tenantRecordId);
 
         // clear tenant-catalog cache by tenantRecordId
-        final Ehcache tenantCatalogCache = cacheManager.getEhcache(CacheType.TENANT_CATALOG.getCacheName());
-        tenantCatalogCache.remove(tenantRecordId);
+        final CacheController<Long, Catalog> tenantCatalogCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_CATALOG);
+        tenantCatalogCacheController.remove(tenantRecordId);
 
         return Response.status(Status.OK).build();
     }
 
-    private void removeCacheByKey(final Ehcache tenantCache, final String tenantRecordId) {
-        for (Object key : tenantCache.getKeys()) {
-            if (null != key && key.toString().endsWith("::" + tenantRecordId)) {
-                tenantCache.remove(key);
-            }
-        }
-    }
-
     private Iterable<NotificationEventWithMetadata<NotificationEvent>> getNotifications(@Nullable final String queueName,
                                                                                         @Nullable final String serviceName,
                                                                                         final boolean includeInProcessing,
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 2d9aff1..8cefa75 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -104,6 +104,7 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 import io.swagger.annotations.Api;
@@ -268,69 +269,8 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                                 @HeaderParam(HDR_COMMENT) final String comment,
                                                 @javax.ws.rs.core.Context final HttpServletRequest request,
                                                 @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
-
-        Preconditions.checkArgument(Iterables.size(entitlements) > 0, "Subscription list mustn't be null or empty.");
-
-        logDeprecationParameterWarningIfNeeded(QUERY_REQUESTED_DT, QUERY_ENTITLEMENT_REQUESTED_DT, QUERY_BILLING_REQUESTED_DT);
-
-        final int baseSubscriptionsSize = Iterables.size(Iterables.filter(entitlements, new Predicate<SubscriptionJson>() {
-            @Override
-            public boolean apply(final SubscriptionJson subscription) {
-                return ProductCategory.BASE.toString().equals(subscription.getProductCategory());
-            }
-        }));
-        verifyNumberOfElements(baseSubscriptionsSize, 1, "Only one BASE product is allowed.");
-
-        final int addOnSubscriptionsSize = Iterables.size(Iterables.filter(entitlements, new Predicate<SubscriptionJson>() {
-            @Override
-            public boolean apply(final SubscriptionJson subscription) {
-                return ProductCategory.ADD_ON.toString().equals(subscription.getProductCategory());
-            }
-        }));
-        verifyNumberOfElements(addOnSubscriptionsSize, entitlements.size() - 1, "It should be " + (entitlements.size() - 1) + " ADD_ON products.");
-
-        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
-        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
-
-        final SubscriptionJson baseEntitlement = Iterables.tryFind(entitlements, new Predicate<SubscriptionJson>() {
-            @Override
-            public boolean apply(final SubscriptionJson subscription) {
-                return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
-            }
-        }).orNull();
-
-        verifyNonNull(baseEntitlement.getAccountId(), "SubscriptionJson accountId needs to be set for BASE product.");
-
-        final EntitlementCallCompletionCallback<List<Entitlement>> callback = new EntitlementCallCompletionCallback<List<Entitlement>>() {
-            @Override
-            public List<Entitlement> doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException, SubscriptionApiException, AccountApiException {
-
-                final Account account = getAccountFromSubscriptionJson(baseEntitlement, callContext);
-
-                final List<EntitlementSpecifier> entitlementSpecifierList = buildEntitlementSpecifierList(entitlements, account.getCurrency());
-
-                final LocalDate resolvedEntitlementDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(entitlementDate);
-                final LocalDate resolvedBillingDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(billingDate);
-
-                final UUID bundleId = baseEntitlement.getBundleId() != null ? UUID.fromString(baseEntitlement.getBundleId()) : null;
-
-                BaseEntitlementWithAddOnsSpecifier baseEntitlementSpecifierWithAddOns = buildBaseEntitlementWithAddOnsSpecifier(entitlementSpecifierList, resolvedEntitlementDate, resolvedBillingDate, bundleId, baseEntitlement, isMigrated);
-                final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
-                baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementSpecifierWithAddOns);
-
-                return entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), baseEntitlementWithAddOnsSpecifierList, pluginProperties, callContext);
-            }
-            @Override
-            public boolean isImmOperation() {
-                return true;
-            }
-            @Override
-            public Response doResponseOk(final List<Entitlement> entitlements) {
-                return uriBuilder.buildResponse(uriInfo, BundleResource.class, "getBundle", entitlements.get(0).getBundleId(), request);
-            }
-        };
-        final EntitlementCallCompletion<List<Entitlement>> callCompletionCreation = new EntitlementCallCompletion<List<Entitlement>>();
-        return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+        final List<BulkBaseSubscriptionAndAddOnsJson> entitlementsWithAddOns = ImmutableList.of(new BulkBaseSubscriptionAndAddOnsJson(entitlements));
+        return createEntitlementsWithAddOnsInternal(entitlementsWithAddOns, requestedDate, entitlementDate, billingDate, isMigrated, callCompletion, timeoutSec, pluginPropertiesString, createdBy, reason, comment, request, uriInfo, ObjectType.BUNDLE);
     }
 
     @TimedResource
@@ -353,6 +293,22 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                                 @HeaderParam(HDR_COMMENT) final String comment,
                                                 @javax.ws.rs.core.Context final HttpServletRequest request,
                                                 @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
+        return createEntitlementsWithAddOnsInternal(entitlementsWithAddOns, requestedDate, entitlementDate, billingDate, isMigrated, callCompletion, timeoutSec, pluginPropertiesString, createdBy, reason, comment, request, uriInfo, ObjectType.ACCOUNT);
+    }
+
+
+    public Response createEntitlementsWithAddOnsInternal(final List<BulkBaseSubscriptionAndAddOnsJson> entitlementsWithAddOns,
+                                                 final String requestedDate,
+                                                 final String entitlementDate,
+                                                 final String billingDate,
+                                                 final Boolean isMigrated, final Boolean callCompletion,
+                                                 final long timeoutSec,
+                                                 final List<String> pluginPropertiesString,
+                                                 final String createdBy,
+                                                 final String reason,
+                                                 final String comment,
+                                                 final HttpServletRequest request,
+                                                 final UriInfo uriInfo, final ObjectType responseObject) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
 
         Preconditions.checkArgument(Iterables.size(entitlementsWithAddOns) > 0, "Subscription bulk list mustn't be null or empty.");
 
@@ -367,35 +323,27 @@ public class SubscriptionResource extends JaxRsResourceBase {
         for (BulkBaseSubscriptionAndAddOnsJson bulkBaseEntitlementWithAddOns : entitlementsWithAddOns) {
             final Iterable<SubscriptionJson> baseEntitlements = Iterables.filter(
                     bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
-                @Override
-                public boolean apply(final SubscriptionJson subscription) {
-                    return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
-                }
-            });
+                        @Override
+                        public boolean apply(final SubscriptionJson subscription) {
+                            return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
+                        }
+                    });
             Preconditions.checkArgument(Iterables.size(baseEntitlements) > 0, "SubscriptionJson Base Entitlement needs to be provided");
             verifyNumberOfElements(Iterables.size(baseEntitlements), 1, "Only one BASE product is allowed per bundle.");
+            final SubscriptionJson baseEntitlement = baseEntitlements.iterator().next();
+
 
-            final Iterable<SubscriptionJson> entitlementsWithBundleSpecified = Iterables.filter(
-                bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
-                    @Override
-                    public boolean apply(final SubscriptionJson subscription) {
-                        return subscription.getBundleId() != null;
+            final Iterable<SubscriptionJson> addonEntitlements = Iterables.filter(
+                    bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
+                        @Override
+                        public boolean apply(final SubscriptionJson subscription) {
+                            return ProductCategory.ADD_ON.toString().equalsIgnoreCase(subscription.getProductCategory());
+                        }
                     }
-                }
-            );
-            Preconditions.checkArgument(Iterables.size(entitlementsWithBundleSpecified) == 0, "BundleId must not be specified when creating new bulks");
+                                                                                 );
 
-            SubscriptionJson baseEntitlement = baseEntitlements.iterator().next();
 
-            final int addOnSubscriptionsSize = Iterables.size(Iterables.filter(bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), new Predicate<SubscriptionJson>() {
-                @Override
-                public boolean apply(final SubscriptionJson subscription) {
-                    return ProductCategory.ADD_ON.toString().equals(subscription.getProductCategory());
-                }
-            }));
-            verifyNumberOfElements(addOnSubscriptionsSize, bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns().size() - 1, "It should be " + (bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns().size() - 1) + " ADD_ON products.");
-
-            final List<EntitlementSpecifier> entitlementSpecifierList = buildEntitlementSpecifierList(bulkBaseEntitlementWithAddOns.getBaseEntitlementAndAddOns(), account.getCurrency());
+            final List<EntitlementSpecifier> entitlementSpecifierList = buildEntitlementSpecifierList(baseEntitlement, addonEntitlements, account.getCurrency());
 
             // create the baseEntitlementSpecifierWithAddOns
             final LocalDate resolvedEntitlementDate = requestedDate != null ? toLocalDate(requestedDate) : toLocalDate(entitlementDate);
@@ -416,16 +364,52 @@ public class SubscriptionResource extends JaxRsResourceBase {
             }
             @Override
             public Response doResponseOk(final List<Entitlement> entitlements) {
-                return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccountBundles", entitlements.get(0).getAccountId(), buildQueryParams(buildBundleIdList(entitlements)), request);
+                if (responseObject == ObjectType.ACCOUNT) {
+                    return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccountBundles", entitlements.get(0).getAccountId(), buildQueryParams(buildBundleIdList(entitlements)), request);
+                } else if (responseObject == ObjectType.BUNDLE) {
+                    return uriBuilder.buildResponse(uriInfo, BundleResource.class, "getBundle", entitlements.get(0).getBundleId(), request);
+                } else {
+                    throw new IllegalStateException("Unexpected input responseObject " + responseObject);
+                }
             }
         };
         final EntitlementCallCompletion<List<Entitlement>> callCompletionCreation = new EntitlementCallCompletion<List<Entitlement>>();
         return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
     }
 
-    private List<EntitlementSpecifier> buildEntitlementSpecifierList(final List<SubscriptionJson> entitlements, final Currency currency) {
+
+
+    private List<EntitlementSpecifier> buildEntitlementSpecifierList(final SubscriptionJson baseEntitlement, final Iterable<SubscriptionJson> addonEntitlements, final Currency currency) {
         final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
-        for (final SubscriptionJson entitlement : entitlements) {
+
+        //
+        // BASE is fully specified we can add it
+        //
+        if (baseEntitlement.getPlanName() != null ||
+            (baseEntitlement.getProductName() != null &&
+             baseEntitlement.getProductCategory() != null &&
+            baseEntitlement.getBillingPeriod() != null &&
+            baseEntitlement.getPriceList() != null)) {
+            final PlanPhaseSpecifier planPhaseSpecifier = baseEntitlement.getPlanName() != null ?
+                                                          new PlanPhaseSpecifier(baseEntitlement.getPlanName(), null) :
+                                                          new PlanPhaseSpecifier(baseEntitlement.getProductName(),
+                                                                                 BillingPeriod.valueOf(baseEntitlement.getBillingPeriod()), baseEntitlement.getPriceList(), null);
+            final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(baseEntitlement.getPriceOverrides(), planPhaseSpecifier, currency);
+
+            EntitlementSpecifier specifier = new EntitlementSpecifier() {
+                @Override
+                public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+                    return planPhaseSpecifier;
+                }
+                @Override
+                public List<PlanPhasePriceOverride> getOverrides() {
+                    return overrides;
+                }
+            };
+            entitlementSpecifierList.add(specifier);
+        }
+
+        for (final SubscriptionJson entitlement : addonEntitlements) {
             // verifications
             verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified for each element");
             if (entitlement.getPlanName() == null) {

junction/pom.xml 2(+1 -1)

diff --git a/junction/pom.xml b/junction/pom.xml
index 6caedca..40b0ae0 100644
--- a/junction/pom.xml
+++ b/junction/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-junction</artifactId>

NEWS 6(+6 -0)

diff --git a/NEWS b/NEWS
index 1ba9b8e..d6374b1 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.18.6
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.18.6
+
+0.18.5
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.18.5
+
 0.18.4
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.4
 

overdue/pom.xml 2(+1 -1)

diff --git a/overdue/pom.xml b/overdue/pom.xml
index 4cd0b83..86f2f21 100644
--- a/overdue/pom.xml
+++ b/overdue/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-overdue</artifactId>
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java b/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
index 7285e87..d5bba20 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/caching/EhCacheOverdueConfigCache.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -44,7 +44,7 @@ public class EhCacheOverdueConfigCache implements OverdueConfigCache {
 
     private static final Logger log = LoggerFactory.getLogger(EhCacheOverdueConfigCache.class);
 
-    private final CacheController cacheController;
+    private final CacheController<Long, OverdueConfig> cacheController;
     private final CacheLoaderArgument cacheLoaderArgument;
 
     private OverdueConfig defaultOverdueConfig;
@@ -91,13 +91,13 @@ public class EhCacheOverdueConfigCache implements OverdueConfigCache {
 
     @Override
     public OverdueConfig getOverdueConfig(final InternalTenantContext tenantContext) throws OverdueApiException {
-        if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId())) {
             return defaultOverdueConfig;
         }
         // The cache loader might choke on some bad xml -- unlikely since we check its validity prior storing it,
         // but to be on the safe side;;
         try {
-            final OverdueConfig overdueConfig = (OverdueConfig) cacheController.get(tenantContext.getTenantRecordId(), cacheLoaderArgument);
+            final OverdueConfig overdueConfig = cacheController.get(tenantContext.getTenantRecordId(), cacheLoaderArgument);
             return (overdueConfig != null) ? overdueConfig : defaultOverdueConfig;
         } catch (final IllegalStateException e) {
             throw new OverdueApiException(ErrorCode.OVERDUE_INVALID_FOR_TENANT, tenantContext.getTenantRecordId());
@@ -106,7 +106,7 @@ public class EhCacheOverdueConfigCache implements OverdueConfigCache {
 
     @Override
     public void clearOverdueConfig(final InternalTenantContext tenantContext) {
-        if (tenantContext.getTenantRecordId() != InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+        if (!InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId())) {
             cacheController.remove(tenantContext.getTenantRecordId());
         }
     }
@@ -114,7 +114,7 @@ public class EhCacheOverdueConfigCache implements OverdueConfigCache {
     private CacheLoaderArgument initializeCacheLoaderArgument() {
         final LoaderCallback loaderCallback = new LoaderCallback() {
             @Override
-            public Object loadOverdueConfig(final String overdueConfigXML) throws OverdueApiException {
+            public OverdueConfig loadOverdueConfig(final String overdueConfigXML) throws OverdueApiException {
                 final InputStream overdueConfigStream = new ByteArrayInputStream(overdueConfigXML.getBytes());
                 final URI uri;
                 try {
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueConfig.java b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueConfig.java
index 9ca684d..0aec675 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueConfig.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/config/DefaultOverdueConfig.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,19 +18,29 @@
 
 package org.killbill.billing.overdue.config;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.net.URI;
+
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
-import java.net.URI;
 
 import org.killbill.billing.overdue.api.OverdueConfig;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 import org.killbill.xmlloader.ValidatingConfig;
 import org.killbill.xmlloader.ValidationErrors;
 
 @XmlRootElement(name = "overdueConfig")
 @XmlAccessorType(XmlAccessType.NONE)
-public class DefaultOverdueConfig extends ValidatingConfig<DefaultOverdueConfig> implements OverdueConfig {
+public class DefaultOverdueConfig extends ValidatingConfig<DefaultOverdueConfig> implements OverdueConfig, Externalizable {
+
+    private static final long serialVersionUID = 1282636602472877120L;
 
     @XmlElement(required = true, name = "accountOverdueStates")
     private DefaultOverdueStatesAccount accountOverdueStates = new DefaultOverdueStatesAccount();
@@ -43,14 +55,25 @@ public class DefaultOverdueConfig extends ValidatingConfig<DefaultOverdueConfig>
         return accountOverdueStates.validate(root, errors);
     }
 
+    // For deserialization
+    public DefaultOverdueConfig() {}
+
     public DefaultOverdueConfig setOverdueStates(final DefaultOverdueStatesAccount accountOverdueStates) {
         this.accountOverdueStates = accountOverdueStates;
         return this;
     }
 
-
     public URI getURI() {
         return null;
     }
 
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
index 43f00c1..8bed766 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/listener/OverdueListener.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -45,6 +45,7 @@ import org.killbill.billing.overdue.notification.OverdueAsyncBusNotificationKey.
 import org.killbill.billing.overdue.notification.OverdueAsyncBusNotifier;
 import org.killbill.billing.overdue.notification.OverduePoster;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.CallOrigin;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
@@ -65,7 +66,7 @@ public class OverdueListener {
     private static final Logger log = LoggerFactory.getLogger(OverdueListener.class);
 
     private final InternalCallContextFactory internalCallContextFactory;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
+    private final CacheController<String, UUID> objectIdCacheController;
     private final Clock clock;
     private final OverduePoster asyncPoster;
     private final OverdueConfigCache overdueConfigCache;
@@ -84,7 +85,7 @@ public class OverdueListener {
         this.clock = clock;
         this.asyncPoster = asyncPoster;
         this.overdueConfigCache = overdueConfigCache;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
+        this.objectIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
         this.internalCallContextFactory = internalCallContextFactory;
         this.accountApi = accountApi;
     }
@@ -96,7 +97,7 @@ public class OverdueListener {
             final InternalCallContext internalCallContext = createCallContext(event.getUserToken(), event.getSearchKey1(), event.getSearchKey2());
             insertBusEventIntoNotificationQueue(event.getObjectId(), OverdueAsyncBusNotificationAction.CLEAR, internalCallContext);
         } else if (event.getTagDefinition().getName().equals(ControlTagType.WRITTEN_OFF.toString()) && event.getObjectType() == ObjectType.INVOICE) {
-            final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+            final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, objectIdCacheController);
             insertBusEventIntoNotificationQueue(accountId, event);
         }
     }
@@ -107,7 +108,7 @@ public class OverdueListener {
         if (event.getTagDefinition().getName().equals(ControlTagType.OVERDUE_ENFORCEMENT_OFF.toString()) && event.getObjectType() == ObjectType.ACCOUNT) {
             insertBusEventIntoNotificationQueue(event.getObjectId(), event);
         } else if (event.getTagDefinition().getName().equals(ControlTagType.WRITTEN_OFF.toString()) && event.getObjectType() == ObjectType.INVOICE) {
-            final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+            final UUID accountId = nonEntityDao.retrieveIdFromObject(event.getSearchKey1(), ObjectType.ACCOUNT, objectIdCacheController);
             insertBusEventIntoNotificationQueue(accountId, event);
         }
     }
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
index ee470c5..9fee915 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/notification/TestOverdueCheckNotifier.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -20,18 +22,17 @@ import java.util.UUID;
 import java.util.concurrent.Callable;
 
 import org.joda.time.DateTime;
-import org.mockito.Mockito;
-import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.overdue.OverdueTestSuiteWithEmbeddedDB;
 import org.killbill.billing.overdue.listener.OverdueDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
-import org.killbill.billing.callcontext.InternalTenantContext;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
 
 import static com.jayway.awaitility.Awaitility.await;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -41,9 +42,7 @@ public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
     private OverdueDispatcherMock mockDispatcher;
     private OverdueNotifier notifierForMock;
 
-
-
-    private static final class  OverdueDispatcherMock extends OverdueDispatcher {
+    private static final class OverdueDispatcherMock extends OverdueDispatcher {
 
         int eventCount = 0;
         UUID latestAccountId = null;
@@ -73,9 +72,6 @@ public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
         //super.beforeMethod();
         // We override the parent method on purpose, because we want to register a different OverdueCheckNotifier
 
-        final Account account = Mockito.mock(Account.class);
-        Mockito.when(accountApi.getAccountById(Mockito.<UUID>any(), Mockito.<InternalTenantContext>any())).thenReturn(account);
-
         mockDispatcher = new OverdueDispatcherMock(internalCallContextFactory);
         notifierForMock = new OverdueCheckNotifier(notificationQueueService, overdueProperties, internalCallContextFactory, mockDispatcher);
 
@@ -95,6 +91,7 @@ public class TestOverdueCheckNotifier extends OverdueTestSuiteWithEmbeddedDB {
         final UUID accountId = new UUID(0L, 1L);
         final Account account = Mockito.mock(Account.class);
         Mockito.when(account.getId()).thenReturn(accountId);
+        Mockito.when(accountApi.getImmutableAccountDataByRecordId(Mockito.<UUID>eq(internalCallContext.getAccountRecordId()), Mockito.<InternalTenantContext>any())).thenReturn(account);
         final DateTime now = clock.getUTCNow();
         final DateTime readyTime = now.plusMillis(2000);
 
diff --git a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java
index f2b590d..b74d629 100644
--- a/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java
+++ b/overdue/src/test/java/org/killbill/billing/overdue/OverdueTestSuiteWithEmbeddedDB.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -21,7 +21,7 @@ package org.killbill.billing.overdue;
 import javax.inject.Named;
 
 import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
-import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.junction.BlockingInternalApi;
 import org.killbill.billing.lifecycle.api.BusService;
@@ -52,7 +52,7 @@ import com.google.inject.Injector;
 public abstract class OverdueTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuiteWithEmbeddedDB {
 
     @Inject
-    protected AccountInternalApi accountApi;
+    protected ImmutableAccountInternalApi accountApi;
     @Inject
     protected BillingStateCalculator calculatorBundle;
     @Inject

payment/pom.xml 6(+1 -5)

diff --git a/payment/pom.xml b/payment/pom.xml
index 0fc7d88..2352511 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-payment</artifactId>
@@ -97,10 +97,6 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>net.sf.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-core</artifactId>
         </dependency>
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
index ae76cb2..29fa335 100644
--- a/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -51,7 +51,7 @@ public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
     private static final Logger logger = LoggerFactory.getLogger(EhCacheStateMachineConfigCache.class);
 
     private final TenantInternalApi tenantInternalApi;
-    private final CacheController cacheController;
+    private final CacheController<String, StateMachineConfig> cacheController;
     private final CacheInvalidationCallback cacheInvalidationCallback;
     private final LoaderCallback loaderCallback;
 
@@ -71,7 +71,8 @@ public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
 
                 try {
                     final InputStream stream = new ByteArrayInputStream(stateMachineConfigXML.getBytes());
-                    return XMLLoader.getObjectFromStream(new URI("dummy"), stream, DefaultStateMachineConfig.class);
+                    final DefaultStateMachineConfig defaultStateMachineConfig = XMLLoader.getObjectFromStream(new URI("dummy"), stream, DefaultStateMachineConfig.class);
+                    return new SerializableStateMachineConfig(defaultStateMachineConfig);
                 } catch (final Exception e) {
                     // TODO 0.17 proper error code
                     throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, "Invalid payment state machine config");
@@ -94,18 +95,18 @@ public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
 
     @Override
     public StateMachineConfig getPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) throws PaymentApiException {
-        if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID || cacheController == null) {
+        if (InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId()) || cacheController == null) {
             return defaultPaymentStateMachineConfig;
         }
 
         final String pluginConfigKey = getCacheKeyName(pluginName, tenantContext);
         final CacheLoaderArgument cacheLoaderArgument = createCacheLoaderArgument(pluginName);
         try {
-            StateMachineConfig pluginPaymentStateMachineConfig = (StateMachineConfig) cacheController.get(pluginConfigKey, cacheLoaderArgument);
+            StateMachineConfig pluginPaymentStateMachineConfig = cacheController.get(pluginConfigKey, cacheLoaderArgument);
             // It means we are using the default state machine config in a multi-tenant deployment
             if (pluginPaymentStateMachineConfig == null) {
                 pluginPaymentStateMachineConfig = defaultPaymentStateMachineConfig;
-                cacheController.add(pluginConfigKey, pluginPaymentStateMachineConfig);
+                cacheController.putIfAbsent(pluginConfigKey, pluginPaymentStateMachineConfig);
             }
             return pluginPaymentStateMachineConfig;
         } catch (final IllegalStateException e) {
@@ -125,7 +126,7 @@ public class EhCacheStateMachineConfigCache implements StateMachineConfigCache {
 
     @Override
     public void clearPaymentStateMachineConfig(final String pluginName, final InternalTenantContext tenantContext) {
-        if (tenantContext.getTenantRecordId() != InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID && cacheController != null) {
+        if (!InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(tenantContext.getTenantRecordId()) && cacheController != null) {
             final String key = getCacheKeyName(pluginName, tenantContext);
             cacheController.remove(key);
         }
diff --git a/payment/src/main/java/org/killbill/billing/payment/caching/SerializableStateMachineConfig.java b/payment/src/main/java/org/killbill/billing/payment/caching/SerializableStateMachineConfig.java
new file mode 100644
index 0000000..d4044b6
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/caching/SerializableStateMachineConfig.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.payment.caching;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
+import org.killbill.automaton.LinkStateMachine;
+import org.killbill.automaton.MissingEntryException;
+import org.killbill.automaton.StateMachine;
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
+
+public class SerializableStateMachineConfig implements StateMachineConfig, Externalizable {
+
+    private static final long serialVersionUID = 945320595649172168L;
+
+    private StateMachineConfig stateMachineConfig;
+
+    // For deserialization
+    public SerializableStateMachineConfig() {}
+
+    public SerializableStateMachineConfig(final StateMachineConfig stateMachineConfig) {
+        this.stateMachineConfig = stateMachineConfig;
+    }
+
+    @Override
+    public StateMachine[] getStateMachines() {
+        return stateMachineConfig.getStateMachines();
+    }
+
+    @Override
+    public LinkStateMachine[] getLinkStateMachines() {
+        return stateMachineConfig.getLinkStateMachines();
+    }
+
+    @Override
+    public StateMachine getStateMachineForState(final String stateName) throws MissingEntryException {
+        return stateMachineConfig.getStateMachineForState(stateName);
+    }
+
+    @Override
+    public StateMachine getStateMachine(final String stateMachineName) throws MissingEntryException {
+        return stateMachineConfig.getStateMachine(stateMachineName);
+    }
+
+    @Override
+    public LinkStateMachine getLinkStateMachine(final String linkStateMachineName) throws MissingEntryException {
+        return stateMachineConfig.getLinkStateMachine(linkStateMachineName);
+    }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 4db7ea5..8cc235e 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -225,14 +225,15 @@ public class PaymentProcessor extends ProcessorBase {
 
     public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final boolean withAttempts, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentModelDao paymentModelDao = paymentDao.getPayment(paymentId, internalTenantContext);
-        if (paymentModelDao == null) {
-            return null;
-        }
-        return toPayment(paymentModelDao, withPluginInfo, withAttempts, properties, tenantContext, internalTenantContext);
+        return getPayment(paymentModelDao, withPluginInfo, withAttempts, properties, tenantContext, internalTenantContext);
     }
 
     public Payment getPaymentByExternalKey(final String paymentExternalKey, final boolean withPluginInfo, final boolean withAttempts, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentModelDao paymentModelDao = paymentDao.getPaymentByExternalKey(paymentExternalKey, internalTenantContext);
+        return getPayment(paymentModelDao, withPluginInfo, withAttempts, properties, tenantContext, internalTenantContext);
+    }
+
+    private Payment getPayment(final PaymentModelDao paymentModelDao, final boolean withPluginInfo, final boolean withAttempts, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         if (paymentModelDao == null) {
             return null;
         }
@@ -546,9 +547,9 @@ public class PaymentProcessor extends ProcessorBase {
             currentStateName = paymentModelDao.getLastSuccessStateName();
         }
 
-        final UUID nonNullPaymentId = paymentAutomatonRunner.run(paymentStateContext, daoHelper, currentStateName, transactionType);
+        paymentAutomatonRunner.run(paymentStateContext, daoHelper, currentStateName, transactionType);
 
-        return getPayment(nonNullPaymentId, true, false, properties, callContext, internalCallContext);
+        return getPayment(paymentStateContext.getPaymentModelDao(), true, false, properties, callContext, internalCallContext);
     }
 
     private void runSanityOnTransactionExternalKey(final Iterable<PaymentTransactionModelDao> allPaymentTransactionsForKey,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
index 5b11b15..70c793a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonDAOHelper.java
@@ -31,6 +31,7 @@ import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.core.PaymentPluginServiceRegistration;
+import org.killbill.billing.payment.dao.PaymentAndTransactionModelDao;
 import org.killbill.billing.payment.dao.PaymentDao;
 import org.killbill.billing.payment.dao.PaymentMethodModelDao;
 import org.killbill.billing.payment.dao.PaymentModelDao;
@@ -86,8 +87,8 @@ public class PaymentAutomatonDAOHelper {
             final PaymentTransactionModelDao newPaymentTransactionModelDao = buildNewPaymentTransactionModelDao(newPaymentModelDao.getId());
 
             existingTransactions = ImmutableList.of();
-            final PaymentModelDao paymentModelDao = paymentDao.insertPaymentWithFirstTransaction(newPaymentModelDao, newPaymentTransactionModelDao, internalCallContext);
-            paymentTransactionModelDao = paymentDao.getTransactionsForPayment(paymentModelDao.getId(), internalCallContext).get(0);
+            final PaymentAndTransactionModelDao paymentAndTransactionModelDao = paymentDao.insertPaymentWithFirstTransaction(newPaymentModelDao, newPaymentTransactionModelDao, internalCallContext);
+            paymentTransactionModelDao = paymentAndTransactionModelDao.getPaymentTransactionModelDao();
 
         } else {
             existingTransactions = paymentDao.getTransactionsForPayment(paymentStateContext.getPaymentId(), internalCallContext);
@@ -131,22 +132,22 @@ public class PaymentAutomatonDAOHelper {
         final String gatewayErrorMsg = paymentInfoPlugin == null ? null : paymentInfoPlugin.getGatewayError();
 
         final String lastSuccessPaymentState = paymentSMHelper.isSuccessState(currentPaymentStateName) ? currentPaymentStateName : null;
-        paymentDao.updatePaymentAndTransactionOnCompletion(paymentStateContext.getAccount().getId(),
-                                                           paymentStateContext.getAttemptId(),
-                                                           paymentStateContext.getPaymentId(),
-                                                           paymentStateContext.getTransactionType(),
-                                                           currentPaymentStateName,
-                                                           lastSuccessPaymentState,
-                                                           paymentStateContext.getPaymentTransactionModelDao().getId(),
-                                                           transactionStatus,
-                                                           processedAmount,
-                                                           processedCurrency,
-                                                           gatewayErrorCode,
-                                                           gatewayErrorMsg,
-                                                           internalCallContext);
-
+        final PaymentAndTransactionModelDao paymentAndTransactionModelDao = paymentDao.updatePaymentAndTransactionOnCompletion(paymentStateContext.getAccount().getId(),
+                                                                                                                               paymentStateContext.getAttemptId(),
+                                                                                                                               paymentStateContext.getPaymentId(),
+                                                                                                                               paymentStateContext.getTransactionType(),
+                                                                                                                               currentPaymentStateName,
+                                                                                                                               lastSuccessPaymentState,
+                                                                                                                               paymentStateContext.getPaymentTransactionModelDao().getId(),
+                                                                                                                               transactionStatus,
+                                                                                                                               processedAmount,
+                                                                                                                               processedCurrency,
+                                                                                                                               gatewayErrorCode,
+                                                                                                                               gatewayErrorMsg,
+                                                                                                                               internalCallContext);
         // Update the context
-        paymentStateContext.setPaymentTransactionModelDao(paymentDao.getPaymentTransaction(paymentStateContext.getPaymentTransactionModelDao().getId(), internalCallContext));
+        paymentStateContext.setPaymentModelDao(paymentAndTransactionModelDao.getPaymentModelDao());
+        paymentStateContext.setPaymentTransactionModelDao(paymentAndTransactionModelDao.getPaymentTransactionModelDao());
     }
 
     public String getPaymentProviderPluginName(final boolean includeDeleted) throws PaymentApiException {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
index 331d317..6c2f77a 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentAutomatonRunner.java
@@ -160,7 +160,7 @@ public class PaymentAutomatonRunner {
         return new PaymentAutomatonDAOHelper(paymentStateContext, utcNow, paymentDao, paymentPluginServiceRegistration, internalCallContext, eventBus, paymentSMHelper);
     }
 
-    public UUID run(final PaymentStateContext paymentStateContext,
+    public void run(final PaymentStateContext paymentStateContext,
                     final PaymentAutomatonDAOHelper daoHelper,
                     @Nullable final String currentStateNameOrNull,
                     final TransactionType transactionType) throws PaymentApiException {
@@ -214,8 +214,6 @@ public class PaymentAutomatonRunner {
         }
 
         runStateMachineOperation(currentStateName, transactionType, leavingStateCallback, operationCallback, enteringStateCallback, includeDeletedPaymentMethod, paymentStateContext, daoHelper);
-
-        return paymentStateContext.getPaymentId();
     }
 
     //
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
index 632f15d..c0d5709 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateContext.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -29,6 +29,7 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.dao.PaymentModelDao;
 import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
 import org.killbill.billing.payment.plugin.api.PaymentTransactionInfoPlugin;
 import org.killbill.billing.util.callcontext.CallContext;
@@ -38,7 +39,7 @@ import com.google.common.collect.ImmutableList;
 
 public class PaymentStateContext {
 
-
+    private PaymentModelDao paymentModelDao;
     // The following fields (paymentId, transactionId, amount, currency) may take their value from the paymentTransactionModelDao *when they are not already set*
     private PaymentTransactionModelDao paymentTransactionModelDao;
     // Initialized in CTOR or only set through paymentTransactionModelDao
@@ -119,6 +120,14 @@ public class PaymentStateContext {
         this.paymentMethodId = paymentMethodId;
     }
 
+    public PaymentModelDao getPaymentModelDao() {
+        return paymentModelDao;
+    }
+
+    public void setPaymentModelDao(final PaymentModelDao paymentModelDao) {
+        this.paymentModelDao = paymentModelDao;
+    }
+
     public PaymentTransactionModelDao getPaymentTransactionModelDao() {
         return paymentTransactionModelDao;
     }
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
index 4d00d40..22baff9 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -107,9 +107,7 @@ public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, P
             @Override
             public PaymentAttemptModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final PaymentAttemptSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class);
-                transactional.create(attempt, context);
-                final PaymentAttemptModelDao result = transactional.getById(attempt.getId().toString(), context);
-                return result;
+                return createAndRefresh(transactional, attempt, context);
             }
         });
     }
@@ -290,16 +288,22 @@ public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, P
     }
 
     @Override
-    public PaymentModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
+    public PaymentAndTransactionModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
+        final PaymentAndTransactionModelDao paymentAndTransactionModelDao = new PaymentAndTransactionModelDao();
 
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
+        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAndTransactionModelDao>() {
 
             @Override
-            public PaymentModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+            public PaymentAndTransactionModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final PaymentSqlDao paymentSqlDao = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
-                paymentSqlDao.create(payment, context);
-                entitySqlDaoWrapperFactory.become(TransactionSqlDao.class).create(paymentTransaction, context);
-                return paymentSqlDao.getById(payment.getId().toString(), context);
+                final PaymentModelDao paymentModelDao = createAndRefresh(paymentSqlDao, payment, context);
+                paymentAndTransactionModelDao.setPaymentModelDao(paymentModelDao);
+
+                final TransactionSqlDao transactionSqlDao = entitySqlDaoWrapperFactory.become(TransactionSqlDao.class);
+                final PaymentTransactionModelDao paymentTransactionModelDao = createAndRefresh(transactionSqlDao, paymentTransaction, context);
+                paymentAndTransactionModelDao.setPaymentTransactionModelDao(paymentTransactionModelDao);
+
+                return paymentAndTransactionModelDao;
             }
         });
     }
@@ -310,8 +314,7 @@ public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, P
             @Override
             public PaymentTransactionModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final TransactionSqlDao transactional = entitySqlDaoWrapperFactory.become(TransactionSqlDao.class);
-                transactional.create(paymentTransaction, context);
-                final PaymentTransactionModelDao paymentTransactionModelDao = transactional.getById(paymentTransaction.getId().toString(), context);
+                final PaymentTransactionModelDao paymentTransactionModelDao = createAndRefresh(transactional, paymentTransaction, context);
 
                 entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updatePaymentForNewTransaction(paymentId.toString(), contextWithUpdatedDate(context));
 
@@ -321,34 +324,50 @@ public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, P
     }
 
     @Override
-    public void updatePaymentAndTransactionOnCompletion(final UUID accountId, @Nullable final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
-                                                        final String currentPaymentStateName, @Nullable final String lastPaymentSuccessStateName,
-                                                        final UUID transactionId, final TransactionStatus transactionStatus,
-                                                        final BigDecimal processedAmount, final Currency processedCurrency,
-                                                        final String gatewayErrorCode, final String gatewayErrorMsg,
-                                                        final InternalCallContext context) {
-        transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+    public PaymentAndTransactionModelDao updatePaymentAndTransactionOnCompletion(final UUID accountId, @Nullable final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
+                                                                                                    final String currentPaymentStateName, @Nullable final String lastPaymentSuccessStateName,
+                                                                                                    final UUID transactionId, final TransactionStatus transactionStatus,
+                                                                                                    final BigDecimal processedAmount, final Currency processedCurrency,
+                                                                                                    final String gatewayErrorCode, final String gatewayErrorMsg,
+                                                                                                    final InternalCallContext context) {
+        final PaymentAndTransactionModelDao paymentAndTransactionModelDao = new PaymentAndTransactionModelDao();
+
+        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAndTransactionModelDao>() {
 
             @Override
-            public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+            public PaymentAndTransactionModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final InternalCallContext contextWithUpdatedDate = contextWithUpdatedDate(context);
+
                 final TransactionSqlDao transactional = entitySqlDaoWrapperFactory.become(TransactionSqlDao.class);
-                final PaymentTransactionModelDao paymentTransactionModelDao = transactional.getById(transactionId.toString(), context);
-                transactional.updateTransactionStatus(transactionId.toString(),
-                                                      attemptId == null ? (paymentTransactionModelDao.getAttemptId() == null ? null : paymentTransactionModelDao.getAttemptId().toString()) : attemptId.toString(),
-                                                      processedAmount,
-                                                      processedCurrency == null ? null : processedCurrency.toString(),
-                                                      transactionStatus == null ? null : transactionStatus.toString(),
-                                                      gatewayErrorCode,
-                                                      gatewayErrorMsg,
-                                                      contextWithUpdatedDate);
+                final String updatedAttemptId;
+                if (attemptId == null) {
+                    final PaymentTransactionModelDao paymentTransactionModelDao = transactional.getById(transactionId.toString(), context);
+                    updatedAttemptId = paymentTransactionModelDao.getAttemptId() == null ? null : paymentTransactionModelDao.getAttemptId().toString();
+                } else {
+                    updatedAttemptId = attemptId.toString();
+                }
+                final PaymentTransactionModelDao paymentTransactionModelDao = (PaymentTransactionModelDao) transactional.updateTransactionStatus(transactionId.toString(),
+                                                                                                                                                 updatedAttemptId,
+                                                                                                                                                 processedAmount,
+                                                                                                                                                 processedCurrency == null ? null : processedCurrency.toString(),
+                                                                                                                                                 transactionStatus == null ? null : transactionStatus.toString(),
+                                                                                                                                                 gatewayErrorCode,
+                                                                                                                                                 gatewayErrorMsg,
+                                                                                                                                                 contextWithUpdatedDate);
+                paymentAndTransactionModelDao.setPaymentTransactionModelDao(paymentTransactionModelDao);
+
+                final PaymentSqlDao paymentSqlDao = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
+                final PaymentModelDao paymentModelDao;
                 if (lastPaymentSuccessStateName != null) {
-                    entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updateLastSuccessPaymentStateName(paymentId.toString(), currentPaymentStateName, lastPaymentSuccessStateName, contextWithUpdatedDate);
+                    paymentModelDao = (PaymentModelDao) paymentSqlDao.updateLastSuccessPaymentStateName(paymentId.toString(), currentPaymentStateName, lastPaymentSuccessStateName, contextWithUpdatedDate);
                 } else {
-                    entitySqlDaoWrapperFactory.become(PaymentSqlDao.class).updatePaymentStateName(paymentId.toString(), currentPaymentStateName, contextWithUpdatedDate);
+                    paymentModelDao = (PaymentModelDao) paymentSqlDao.updatePaymentStateName(paymentId.toString(), currentPaymentStateName, contextWithUpdatedDate);
                 }
+                paymentAndTransactionModelDao.setPaymentModelDao(paymentModelDao);
+
                 postPaymentEventFromTransaction(accountId, transactionStatus, transactionType, paymentId, transactionId, processedAmount, processedCurrency, clock.getUTCNow(), gatewayErrorCode, entitySqlDaoWrapperFactory, context);
-                return null;
+
+                return paymentAndTransactionModelDao;
             }
         });
     }
@@ -428,9 +447,7 @@ public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, P
     private PaymentMethodModelDao insertPaymentMethodInTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory, final PaymentMethodModelDao paymentMethod, final InternalCallContext context)
             throws EntityPersistenceException {
         final PaymentMethodSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class);
-        transactional.create(paymentMethod, context);
-
-        return transactional.getById(paymentMethod.getId().toString(), context);
+        return createAndRefresh(transactional, paymentMethod, context);
     }
 
     @Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAndTransactionModelDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAndTransactionModelDao.java
new file mode 100644
index 0000000..96d6987
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentAndTransactionModelDao.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.payment.dao;
+
+public class PaymentAndTransactionModelDao {
+
+    private PaymentModelDao paymentModelDao;
+    private PaymentTransactionModelDao paymentTransactionModelDao;
+
+    public PaymentModelDao getPaymentModelDao() {
+        return paymentModelDao;
+    }
+
+    public void setPaymentModelDao(final PaymentModelDao paymentModelDao) {
+        this.paymentModelDao = paymentModelDao;
+    }
+
+    public PaymentTransactionModelDao getPaymentTransactionModelDao() {
+        return paymentTransactionModelDao;
+    }
+
+    public void setPaymentTransactionModelDao(final PaymentTransactionModelDao paymentTransactionModelDao) {
+        this.paymentTransactionModelDao = paymentTransactionModelDao;
+    }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
index 2c46e15..b7180e8 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -55,13 +57,13 @@ public interface PaymentDao extends EntityDao<PaymentModelDao, Payment, PaymentA
 
     public Pagination<PaymentModelDao> searchPayments(String searchKey, Long offset, Long limit, InternalTenantContext context);
 
-    public PaymentModelDao insertPaymentWithFirstTransaction(PaymentModelDao payment, PaymentTransactionModelDao paymentTransaction, InternalCallContext context);
+    public PaymentAndTransactionModelDao insertPaymentWithFirstTransaction(PaymentModelDao payment, PaymentTransactionModelDao paymentTransaction, InternalCallContext context);
 
     public PaymentTransactionModelDao updatePaymentWithNewTransaction(UUID paymentId, PaymentTransactionModelDao paymentTransaction, InternalCallContext context);
 
-    public void updatePaymentAndTransactionOnCompletion(UUID accountId, UUID attemptId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
-                                                        TransactionStatus paymentStatus, BigDecimal processedAmount, Currency processedCurrency,
-                                                        String gatewayErrorCode, String gatewayErrorMsg, InternalCallContext context);
+    public PaymentAndTransactionModelDao updatePaymentAndTransactionOnCompletion(UUID accountId, UUID attemptId, UUID paymentId, TransactionType transactionType, String currentPaymentStateName, String lastPaymentSuccessStateName, UUID transactionId,
+                                                                                 TransactionStatus paymentStatus, BigDecimal processedAmount, Currency processedCurrency,
+                                                                                 String gatewayErrorCode, String gatewayErrorMsg, InternalCallContext context);
 
     public PaymentModelDao getPayment(UUID paymentId, InternalTenantContext context);
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
index 2be09a6..ee8ad0f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
@@ -45,17 +45,16 @@ public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
-    void updatePaymentStateName(@Bind("id") final String paymentId,
-                                @Bind("stateName") final String stateName,
-                                @BindBean final InternalCallContext context);
-
+    Object updatePaymentStateName(@Bind("id") final String paymentId,
+                                  @Bind("stateName") final String stateName,
+                                  @BindBean final InternalCallContext context);
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
-    void updateLastSuccessPaymentStateName(@Bind("id") final String paymentId,
-                                @Bind("stateName") final String stateName,
-                                @Bind("lastSuccessStateName") final String lastSuccessStateName,
-                                @BindBean final InternalCallContext context);
+    Object updateLastSuccessPaymentStateName(@Bind("id") final String paymentId,
+                                             @Bind("stateName") final String stateName,
+                                             @Bind("lastSuccessStateName") final String lastSuccessStateName,
+                                             @BindBean final InternalCallContext context);
 
     @SqlQuery
     public PaymentModelDao getPaymentByExternalKey(@Bind("externalKey") final String externalKey,
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
index 4f687aa..ba8b739 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/TransactionSqlDao.java
@@ -42,14 +42,14 @@ public interface TransactionSqlDao extends EntitySqlDao<PaymentTransactionModelD
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
-    void updateTransactionStatus(@Bind("id") final String transactionId,
-                                 @Bind("attemptId") final String attemptId,
-                                 @Bind("processedAmount") final BigDecimal processedAmount,
-                                 @Bind("processedCurrency") final String processedCurrency,
-                                 @Bind("transactionStatus") final String transactionStatus,
-                                 @Bind("gatewayErrorCode") final String gatewayErrorCode,
-                                 @Bind("gatewayErrorMsg") final String gatewayErrorMsg,
-                                 @BindBean final InternalCallContext context);
+    Object updateTransactionStatus(@Bind("id") final String transactionId,
+                                   @Bind("attemptId") final String attemptId,
+                                   @Bind("processedAmount") final BigDecimal processedAmount,
+                                   @Bind("processedCurrency") final String processedCurrency,
+                                   @Bind("transactionStatus") final String transactionStatus,
+                                   @Bind("gatewayErrorCode") final String gatewayErrorCode,
+                                   @Bind("gatewayErrorMsg") final String gatewayErrorMsg,
+                                   @BindBean final InternalCallContext context);
 
     @SqlQuery
     List<PaymentTransactionModelDao> getPaymentTransactionsByExternalKey(@Bind("transactionExternalKey") final String transactionExternalKey,
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
index 9960e7a..cb2383a 100644
--- a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCache.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -38,7 +38,6 @@ import org.testng.annotations.Test;
 
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Resources;
-import net.sf.ehcache.CacheException;
 
 public class TestStateMachineConfigCache extends PaymentTestSuiteNoDB {
 
@@ -82,7 +81,7 @@ public class TestStateMachineConfigCache extends PaymentTestSuiteNoDB {
             @Override
             public String answer(final InvocationOnMock invocation) throws Throwable {
                 if (shouldThrow.get()) {
-                    throw new RuntimeException();
+                    throw new RuntimeException("For test purposes");
                 }
                 final InternalTenantContext internalContext = (InternalTenantContext) invocation.getArguments()[1];
                 if (multiTenantRecordId.equals(internalContext.getTenantRecordId())) {
@@ -141,8 +140,9 @@ public class TestStateMachineConfigCache extends PaymentTestSuiteNoDB {
         try {
             stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
             Assert.fail();
-        } catch (final CacheException exception) {
+        } catch (final RuntimeException exception) {
             Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+            Assert.assertEquals(exception.getCause().getMessage(), "For test purposes");
         }
     }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
index 9ead7cd..18b1f31 100644
--- a/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/caching/TestStateMachineConfigCacheInvalidationCallback.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -32,8 +32,6 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import net.sf.ehcache.CacheException;
-
 public class TestStateMachineConfigCacheInvalidationCallback extends PaymentTestSuiteNoDB {
 
     private InternalTenantContext multiTenantContext;
@@ -67,7 +65,7 @@ public class TestStateMachineConfigCacheInvalidationCallback extends PaymentTest
             @Override
             public String answer(final InvocationOnMock invocation) throws Throwable {
                 if (shouldThrow.get()) {
-                    throw new RuntimeException();
+                    throw new RuntimeException("For test purposes");
                 }
                 return null;
             }
@@ -87,8 +85,9 @@ public class TestStateMachineConfigCacheInvalidationCallback extends PaymentTest
         try {
             stateMachineConfigCache.getPaymentStateMachineConfig(pluginName, multiTenantContext);
             Assert.fail();
-        } catch (final CacheException exception) {
+        } catch (final RuntimeException exception) {
             Assert.assertTrue(exception.getCause() instanceof RuntimeException);
+            Assert.assertEquals(exception.getCause().getMessage(), "For test purposes");
         }
 
         // No exception (cached)
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
index d6e79c8..deee81e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryAuthorizeOperationCallback.java
@@ -87,7 +87,7 @@ public class MockRetryAuthorizeOperationCallback extends AuthorizeControlOperati
                                                                                       paymentStateContext.getCurrency(),
                                                                                       "",
                                                                                       "");
-        final PaymentModelDao paymentModelDao = paymentDao.insertPaymentWithFirstTransaction(payment, transaction, paymentStateContext.getInternalCallContext());
+        final PaymentModelDao paymentModelDao = paymentDao.insertPaymentWithFirstTransaction(payment, transaction, paymentStateContext.getInternalCallContext()).getPaymentModelDao();
         final PaymentTransaction convertedTransaction = new DefaultPaymentTransaction(transaction.getId(),
                                                                                                   paymentStateContext.getAttemptId(),
                                                                                                   transaction.getTransactionExternalKey(),
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
index 79f4b34..3912a51 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/TestPaymentMethodProcessorWithDB.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -17,12 +17,18 @@
 
 package org.killbill.billing.payment.core;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentMethod;
 import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.dao.PaymentMethodModelDao;
+import org.killbill.billing.payment.dao.PaymentMethodSqlDao;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -55,4 +61,43 @@ public class TestPaymentMethodProcessorWithDB extends PaymentTestSuiteWithEmbedd
         final Account accountById = accountApi.getAccountById(account.getId(), internalCallContext);
         Assert.assertEquals(accountById.getPaymentMethodId(), newPaymentMethod);
     }
+
+    @Test(groups = "slow")
+    public void testDeletePaymentMethod() throws Exception {
+        final Account account = testHelper.createTestAccount("foo@bar.com", true);
+
+        final UUID paymentMethodId = paymentMethodProcessor.createOrGetExternalPaymentMethod("pmExternalKey", account, PLUGIN_PROPERTIES, callContext, internalCallContext);
+        final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethod(paymentMethodId, internalCallContext);
+
+        final List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> history1 = getPaymentMethodHistory(paymentMethodModelDao.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        Assert.assertTrue(history1.get(0).getEntity().isActive());
+
+        paymentMethodProcessor.deletedPaymentMethod(account, paymentMethodId, true, true, ImmutableList.<PluginProperty>of(), callContext, internalCallContext);
+
+        final List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> history2 = getPaymentMethodHistory(paymentMethodModelDao.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(0).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        Assert.assertTrue(history2.get(0).getEntity().isActive());
+        // Note: it looks like we don't consider this as a DELETE, probably because we can un-delete such payment methods?
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), paymentMethodModelDao.getAccountRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), paymentMethodModelDao.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), paymentMethodModelDao.getExternalKey());
+        // Note: upon deletion, the recorded state is the same as before the delete
+        Assert.assertTrue(history2.get(1).getEntity().isActive());
+    }
+
+    private List<EntityHistoryModelDao<PaymentMethodModelDao, PaymentMethod>> getPaymentMethodHistory(final Long paymentMethodRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final PaymentMethodSqlDao paymentMethodSqlDao = dbi.onDemand(PaymentMethodSqlDao.class);
+        return paymentMethodSqlDao.getHistoryForTargetRecordId(paymentMethodRecordId, internalCallContext);
+    }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index 12ce2be..f167975 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -191,7 +191,8 @@ public class MockPaymentDao extends MockEntityDaoBase<PaymentModelDao, Payment, 
     }
 
     @Override
-    public PaymentModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
+    public PaymentAndTransactionModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
+        final PaymentAndTransactionModelDao paymentAndTransactionModelDao = new PaymentAndTransactionModelDao();
 
         payment.setTenantRecordId(context.getTenantRecordId());
         paymentTransaction.setTenantRecordId(context.getTenantRecordId());
@@ -207,7 +208,11 @@ public class MockPaymentDao extends MockEntityDaoBase<PaymentModelDao, Payment, 
             mockNonEntityDao.addTenantRecordIdMapping(paymentTransaction.getId(), context);
             mockNonEntityDao.addAccountRecordIdMapping((paymentTransaction.getId()), context);
         }
-        return payment;
+
+        paymentAndTransactionModelDao.setPaymentModelDao(payment);
+        paymentAndTransactionModelDao.setPaymentTransactionModelDao(paymentTransaction);
+
+        return paymentAndTransactionModelDao;
     }
 
     @Override
@@ -224,10 +229,12 @@ public class MockPaymentDao extends MockEntityDaoBase<PaymentModelDao, Payment, 
     }
 
     @Override
-    public void updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
-                                                        final String currentPaymentStateName, final String lastSuccessPaymentStateName, final UUID transactionId,
-                                                        final TransactionStatus paymentStatus, final BigDecimal processedAmount, final Currency processedCurrency,
-                                                        final String gatewayErrorCode, final String gatewayErrorMsg, final InternalCallContext context) {
+    public PaymentAndTransactionModelDao updatePaymentAndTransactionOnCompletion(final UUID accountId, final UUID attemptId, final UUID paymentId, final TransactionType transactionType,
+                                                                                 final String currentPaymentStateName, final String lastSuccessPaymentStateName, final UUID transactionId,
+                                                                                 final TransactionStatus paymentStatus, final BigDecimal processedAmount, final Currency processedCurrency,
+                                                                                 final String gatewayErrorCode, final String gatewayErrorMsg, final InternalCallContext context) {
+        final PaymentAndTransactionModelDao paymentAndTransactionModelDao = new PaymentAndTransactionModelDao();
+
         synchronized (this) {
             final PaymentModelDao payment = payments.get(paymentId);
             if (payment != null) {
@@ -242,6 +249,11 @@ public class MockPaymentDao extends MockEntityDaoBase<PaymentModelDao, Payment, 
                 transaction.setGatewayErrorCode(gatewayErrorCode);
                 transaction.setGatewayErrorMsg(gatewayErrorMsg);
             }
+
+            paymentAndTransactionModelDao.setPaymentModelDao(payment);
+            paymentAndTransactionModelDao.setPaymentTransactionModelDao(transaction);
+
+            return paymentAndTransactionModelDao;
         }
     }
 
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
index 35d2c42..0fa8999 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -50,7 +50,7 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         final PaymentTransactionModelDao specifiedFirstPaymentTransactionModelDao = generatePaymentTransactionModelDao(specifiedFirstPaymentModelDao.getId());
 
         // Create and verify the payment and transaction
-        final PaymentModelDao firstPaymentModelDao = paymentDao.insertPaymentWithFirstTransaction(specifiedFirstPaymentModelDao, specifiedFirstPaymentTransactionModelDao, internalCallContext);
+        final PaymentModelDao firstPaymentModelDao = paymentDao.insertPaymentWithFirstTransaction(specifiedFirstPaymentModelDao, specifiedFirstPaymentTransactionModelDao, internalCallContext).getPaymentModelDao();
         verifyPayment(firstPaymentModelDao, specifiedFirstPaymentModelDao);
         verifyPaymentAndTransactions(internalCallContext, specifiedFirstPaymentModelDao, specifiedFirstPaymentTransactionModelDao);
 
@@ -89,7 +89,7 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
             final PaymentModelDao paymentModelDao = generatePaymentModelDao(accountId);
             final PaymentTransactionModelDao paymentTransactionModelDao = generatePaymentTransactionModelDao(paymentModelDao.getId());
 
-            final PaymentModelDao insertedPaymentModelDao = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, paymentTransactionModelDao, internalCallContext);
+            final PaymentModelDao insertedPaymentModelDao = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, paymentTransactionModelDao, internalCallContext).getPaymentModelDao();
             verifyPayment(insertedPaymentModelDao, paymentModelDao);
 
             // Verify search APIs
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
index a195593..113c147 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestPaymentDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -28,13 +28,14 @@ import org.joda.time.DateTime;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.Payment;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.killbill.billing.util.entity.Pagination;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -95,7 +96,6 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
 
     @Test(groups = "slow")
     public void testPaymentAndTransactions() {
-
         final UUID paymentMethodId = UUID.randomUUID();
         final UUID accountId = UUID.randomUUID();
         final String externalKey = "hhhhooo";
@@ -110,13 +110,24 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
                                                                                               TransactionStatus.SUCCESS, BigDecimal.TEN, Currency.AED,
                                                                                               "success", "");
 
-        final PaymentModelDao savedPayment = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, transactionModelDao, internalCallContext);
+        final PaymentModelDao savedPayment = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, transactionModelDao, internalCallContext).getPaymentModelDao();
         assertEquals(savedPayment.getId(), paymentModelDao.getId());
         assertEquals(savedPayment.getAccountId(), paymentModelDao.getAccountId());
         assertEquals(savedPayment.getExternalKey(), paymentModelDao.getExternalKey());
         assertEquals(savedPayment.getPaymentMethodId(), paymentModelDao.getPaymentMethodId());
         assertNull(savedPayment.getStateName());
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history1 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history1.size(), 1);
+        Assert.assertEquals(history1.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history1.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history1.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history1.get(0).getEntity().getStateName(), savedPayment.getStateName());
+        Assert.assertEquals(history1.get(0).getEntity().getLastSuccessStateName(), savedPayment.getLastSuccessStateName());
+        Assert.assertNull(history1.get(0).getEntity().getStateName());
+        Assert.assertNull(history1.get(0).getEntity().getLastSuccessStateName());
+
         final PaymentModelDao savedPayment2 = paymentDao.getPayment(savedPayment.getId(), internalCallContext);
         assertEquals(savedPayment2.getId(), paymentModelDao.getId());
         assertEquals(savedPayment2.getAccountId(), paymentModelDao.getAccountId());
@@ -162,6 +173,20 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedTransactionModelDao2.getAmount().compareTo(BigDecimal.TEN), 0);
         assertEquals(savedTransactionModelDao2.getCurrency(), Currency.AED);
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history2 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history2.size(), 2);
+        Assert.assertEquals(history2.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history2.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history2.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history2.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history2.get(1).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history2.get(1).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history2.get(1).getEntity().getUpdatedDate().compareTo(history2.get(0).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertNull(history2.get(1).getEntity().getStateName());
+        Assert.assertNull(history2.get(1).getEntity().getLastSuccessStateName());
+
         final List<PaymentTransactionModelDao> transactions = paymentDao.getTransactionsForPayment(savedPayment.getId(), internalCallContext);
         assertEquals(transactions.size(), 2);
 
@@ -176,6 +201,25 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
         assertEquals(savedPayment4.getStateName(), "AUTH_ABORTED");
         assertEquals(savedPayment4.getLastSuccessStateName(), "AUTH_SUCCESS");
 
+        final List<EntityHistoryModelDao<PaymentModelDao, Payment>> history3 = getPaymentHistory(savedPayment.getRecordId());
+        Assert.assertEquals(history3.size(), 3);
+        Assert.assertEquals(history3.get(0).getChangeType(), ChangeType.INSERT);
+        Assert.assertEquals(history3.get(0).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(0).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(0).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertEquals(history3.get(1).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(1).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(1).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(1).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history3.get(1).getEntity().getUpdatedDate().compareTo(history3.get(0).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertEquals(history3.get(2).getChangeType(), ChangeType.UPDATE);
+        Assert.assertEquals(history3.get(2).getEntity().getAccountRecordId(), savedPayment.getAccountRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getTenantRecordId(), savedPayment.getTenantRecordId());
+        Assert.assertEquals(history3.get(2).getEntity().getExternalKey(), savedPayment.getExternalKey());
+        Assert.assertTrue(history3.get(2).getEntity().getUpdatedDate().compareTo(history3.get(2).getEntity().getUpdatedDate()) >= 0);
+        Assert.assertEquals(history3.get(2).getEntity().getStateName(), savedPayment4.getStateName());
+        Assert.assertEquals(history3.get(2).getEntity().getLastSuccessStateName(), savedPayment4.getLastSuccessStateName());
+
         final PaymentTransactionModelDao savedTransactionModelDao4 = paymentDao.getPaymentTransaction(savedTransactionModelDao2.getId(), internalCallContext);
         assertEquals(savedTransactionModelDao4.getTransactionExternalKey(), transactionExternalKey2);
         assertEquals(savedTransactionModelDao4.getPaymentId(), paymentModelDao.getId());
@@ -268,7 +312,7 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
                                                                                        TransactionStatus.PENDING, BigDecimal.TEN, Currency.AED,
                                                                                        "pending", "");
 
-        final PaymentModelDao payment = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, transaction1, internalCallContext);
+        final PaymentModelDao payment = paymentDao.insertPaymentWithFirstTransaction(paymentModelDao, transaction1, internalCallContext).getPaymentModelDao();
 
         final PaymentTransactionModelDao transaction2 = new PaymentTransactionModelDao(initialTime, initialTime, null, transactionExternalKey2,
                                                                                        paymentModelDao.getId(), TransactionType.AUTHORIZE, initialTime,
@@ -550,5 +594,11 @@ public class TestPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
             }
         }));
     }
+
+    private List<EntityHistoryModelDao<PaymentModelDao, Payment>> getPaymentHistory(final Long paymentRecordId) {
+        // See https://github.com/killbill/killbill/issues/335
+        final PaymentSqlDao paymentSqlDao = dbi.onDemand(PaymentSqlDao.class);
+        return paymentSqlDao.getHistoryForTargetRecordId(paymentRecordId, internalCallContext);
+    }
 }
 

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 2c2ec75..af7841c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
         <version>0.141-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
-    <version>0.18.5-SNAPSHOT</version>
+    <version>0.18.7-SNAPSHOT</version>
     <packaging>pom</packaging>
     <name>killbill</name>
     <description>Library for managing recurring subscriptions and the associated billing</description>
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index 1e98233..d8ad48c 100644
--- a/profiles/killbill/pom.xml
+++ b/profiles/killbill/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killbill</artifactId>
@@ -172,10 +172,6 @@
         </dependency>
         <dependency>
             <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-ehcache</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shiro</groupId>
             <artifactId>shiro-guice</artifactId>
         </dependency>
         <dependency>
@@ -228,6 +224,10 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.ehcache.integrations.shiro</groupId>
+            <artifactId>shiro-ehcache3</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.javassist</groupId>
             <artifactId>javassist</artifactId>
             <scope>runtime</scope>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
index 05f7991..a850c5b 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/PatternObfuscator.java
@@ -43,6 +43,7 @@ public class PatternObfuscator extends Obfuscator {
             "ccvv",
             "cvNumber",
             "cvc",
+            "cvv",
             "email",
             "iban",
             "name",
@@ -76,7 +77,7 @@ public class PatternObfuscator extends Obfuscator {
     }
 
     private Pattern buildJSONPattern(final String key) {
-        return Pattern.compile(key + "\":\\s*([^,{]+)", DEFAULT_PATTERN_FLAGS);
+        return Pattern.compile(key + "\"\\s*:\\s*([^,{}]+)", DEFAULT_PATTERN_FLAGS);
     }
 
     private Pattern buildXMLPattern(final String key) {
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
index d53b571..5774818 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -23,7 +23,6 @@ import javax.sql.DataSource;
 import org.apache.shiro.cache.CacheManager;
 import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
 import org.killbill.billing.util.config.definition.SecurityConfig;
-import org.killbill.billing.util.glue.ShiroEhCacheInstrumentor;
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,14 +31,12 @@ public class KillbillJdbcTenantRealmProvider implements Provider<KillbillJdbcTen
 
     private final SecurityConfig securityConfig;
     private final CacheManager cacheManager;
-    private final ShiroEhCacheInstrumentor ehCacheInstrumentor;
     private final DataSource dataSource;
 
     @Inject
-    public KillbillJdbcTenantRealmProvider(final SecurityConfig securityConfig, final CacheManager cacheManager, final ShiroEhCacheInstrumentor ehCacheInstrumentor, @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource) {
+    public KillbillJdbcTenantRealmProvider(final SecurityConfig securityConfig, final CacheManager cacheManager, @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource) {
         this.securityConfig = securityConfig;
         this.cacheManager = cacheManager;
-        this.ehCacheInstrumentor = ehCacheInstrumentor;
         this.dataSource = dataSource;
     }
 
@@ -52,9 +49,6 @@ public class KillbillJdbcTenantRealmProvider implements Provider<KillbillJdbcTen
         // automatically configured with the EhCache manager (see EhCacheManagerProvider)
         killbillJdbcTenantRealm.setCacheManager(cacheManager);
 
-        // Instrument the cache
-        ehCacheInstrumentor.instrument(killbillJdbcTenantRealm);
-
         return killbillJdbcTenantRealm;
     }
 }
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index a623ff3..49057fb 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -27,32 +27,27 @@ import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
 import org.apache.shiro.authc.pam.ModularRealmAuthenticatorWith540;
 import org.apache.shiro.cache.CacheManager;
 import org.apache.shiro.guice.web.ShiroWebModuleWith435;
-import org.apache.shiro.realm.Realm;
 import org.apache.shiro.session.mgt.SessionManager;
-import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
 import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
 import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
 import org.apache.shiro.web.mgt.WebSecurityManager;
-import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
 import org.apache.shiro.web.util.WebUtils;
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
 import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
+import org.killbill.billing.server.security.KillBillWebSessionManager;
 import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
 import org.killbill.billing.util.config.definition.RbacConfig;
-import org.killbill.billing.util.glue.EhCacheManagerProvider;
+import org.killbill.billing.util.glue.EhcacheShiroManagerProvider;
 import org.killbill.billing.util.glue.IniRealmProvider;
 import org.killbill.billing.util.glue.JDBCSessionDaoProvider;
 import org.killbill.billing.util.glue.KillBillShiroModule;
-import org.killbill.billing.util.glue.ShiroEhCacheInstrumentor;
 import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
 import org.skife.config.ConfigSource;
 import org.skife.config.ConfigurationObjectFactory;
 
-import com.google.inject.Inject;
 import com.google.inject.Key;
-import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.binder.AnnotatedBindingBuilder;
 import com.google.inject.matcher.AbstractMatcher;
@@ -73,16 +68,9 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
     }
 
     @Override
-    public void configure() {
-        super.configure();
-
-        bind(ShiroEhCacheInstrumentor.class).asEagerSingleton();
-    }
-
-    @Override
     protected void configureShiroWeb() {
         // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+        bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
 
         configureShiroForRBAC();
 
@@ -106,7 +94,7 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
                              return Matchers.subclassesOf(WebSecurityManager.class).matches(o.getRawType());
                          }
                      },
-                     new DefaultWebSecurityManagerTypeListener(getProvider(ShiroEhCacheInstrumentor.class)));
+                     new DefaultWebSecurityManagerTypeListener());
 
         if (KillBillShiroModule.isRBACEnabled()) {
             addFilterChain(JaxrsResource.PREFIX + "/**", Key.get(CorsBasicHttpAuthenticationFilter.class));
@@ -123,7 +111,7 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
     protected void bindSessionManager(final AnnotatedBindingBuilder<SessionManager> bind) {
         // Bypass the servlet container completely for session management and delegate it to Shiro.
         // The default session timeout is 30 minutes.
-        bind.to(DefaultWebSessionManager.class).asEagerSingleton();
+        bind.to(KillBillWebSessionManager.class).asEagerSingleton();
 
         // Magic provider to configure the session DAO
         bind(JDBCSessionDao.class).toProvider(JDBCSessionDaoProvider.class).asEagerSingleton();
@@ -143,30 +131,16 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
 
     private static final class DefaultWebSecurityManagerTypeListener implements TypeListener {
 
-        private final Provider<ShiroEhCacheInstrumentor> instrumentorProvider;
-
-        @Inject
-        public DefaultWebSecurityManagerTypeListener(final Provider<ShiroEhCacheInstrumentor> instrumentorProvider) {
-            this.instrumentorProvider = instrumentorProvider;
-        }
-
         @Override
         public <I> void hear(final TypeLiteral<I> typeLiteral, final TypeEncounter<I> typeEncounter) {
             typeEncounter.register(new InjectionListener<I>() {
                 @Override
                 public void afterInjection(final Object o) {
-                    final ShiroEhCacheInstrumentor ehCacheInstrumentor = instrumentorProvider.get();
-                    ehCacheInstrumentor.instrument(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
-
                     final DefaultWebSecurityManager webSecurityManager = (DefaultWebSecurityManager) o;
                     if (webSecurityManager.getAuthenticator() instanceof ModularRealmAuthenticator) {
                         final ModularRealmAuthenticator authenticator = (ModularRealmAuthenticator) webSecurityManager.getAuthenticator();
                         authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategyWith540());
                         webSecurityManager.setAuthenticator(new ModularRealmAuthenticatorWith540(authenticator));
-
-                        for (final Realm realm : webSecurityManager.getRealms()) {
-                            ehCacheInstrumentor.instrument(realm);
-                        }
                     }
                 }
             });
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
index aa2e377..7faf4f6 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,6 +18,11 @@
 
 package org.killbill.billing.server.security;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
 import javax.sql.DataSource;
 
 import org.apache.shiro.authc.AuthenticationException;
@@ -25,8 +30,12 @@ import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.authc.SimpleAuthenticationInfo;
 import org.apache.shiro.codec.Base64;
+import org.apache.shiro.codec.Hex;
 import org.apache.shiro.realm.jdbc.JdbcRealm;
 import org.apache.shiro.util.ByteSource;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 import org.killbill.billing.util.config.definition.SecurityConfig;
 import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher;
 
@@ -61,7 +70,9 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
 
         // We store the salt bytes in Base64 (because the JdbcRealm retrieves it as a String)
         final ByteSource base64Salt = authenticationInfo.getCredentialsSalt();
-        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(base64Salt.getBytes())));
+        final byte[] bytes = Base64.decode(base64Salt.getBytes());
+        // SimpleByteSource isn't Serializable
+        authenticationInfo.setCredentialsSalt(new SerializableSimpleByteSource(bytes));
 
         return authenticationInfo;
     }
@@ -78,4 +89,56 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
     private void configureDataSource() {
         setDataSource(dataSource);
     }
+
+    private static final class SerializableSimpleByteSource implements ByteSource, Externalizable {
+
+        private static final long serialVersionUID = 4498655519894503985L;
+
+        private byte[] bytes;
+        private String cachedHex;
+        private String cachedBase64;
+
+        // For deserialization
+        public SerializableSimpleByteSource() {}
+
+        SerializableSimpleByteSource(final byte[] bytes) {
+            this.bytes = bytes;
+        }
+
+        @Override
+        public byte[] getBytes() {
+            return bytes;
+        }
+
+        @Override
+        public String toHex() {
+            if (this.cachedHex == null) {
+                this.cachedHex = Hex.encodeToString(getBytes());
+            }
+            return this.cachedHex;
+        }
+
+        @Override
+        public String toBase64() {
+            if (this.cachedBase64 == null) {
+                this.cachedBase64 = Base64.encodeToString(getBytes());
+            }
+            return this.cachedBase64;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return this.bytes == null || this.bytes.length == 0;
+        }
+
+        @Override
+        public void readExternal(final ObjectInput in) throws IOException {
+            MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+        }
+
+        @Override
+        public void writeExternal(final ObjectOutput oo) throws IOException {
+            MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+        }
+    }
 }
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillBillWebSessionManager.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillBillWebSessionManager.java
new file mode 100644
index 0000000..e67ec0e
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillBillWebSessionManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.server.security;
+
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.SessionContext;
+import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
+
+public class KillBillWebSessionManager extends DefaultWebSessionManager {
+
+    @Override
+    protected Session newSessionInstance(final SessionContext context) {
+        final Session session = super.newSessionInstance(context);
+
+        // DefaultWebSessionManager will call applyGlobalSessionTimeout() in
+        // start() below instead, which in turn calls onChange() and triggers a DAO UPDATE call
+        session.setTimeout(getGlobalSessionTimeout());
+
+        return session;
+    }
+
+    @Override
+    public Session start(final SessionContext context) {
+        final Session session = createSession(context);
+
+        // See above
+        //applyGlobalSessionTimeout(session);
+
+        onStart(session, context);
+        notifyStart(session);
+        return createExposedSession(session, context);
+    }
+}
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCache.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCache.java
index 23325ad..c00915a 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCache.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCache.java
@@ -16,11 +16,13 @@
 
 package org.killbill.billing.jaxrs;
 
-import java.util.List;
 import java.util.UUID;
 
 import org.joda.time.LocalDate;
+import org.killbill.automaton.StateMachineConfig;
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.client.RequestOptions;
@@ -29,12 +31,14 @@ import org.killbill.billing.client.model.PaymentMethod;
 import org.killbill.billing.client.model.PaymentMethodPluginDetail;
 import org.killbill.billing.client.model.Subscription;
 import org.killbill.billing.client.model.Tenant;
+import org.killbill.billing.overdue.api.OverdueConfig;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
+import org.killbill.billing.util.config.tenant.PerTenantConfig;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.google.common.io.Resources;
-import net.sf.ehcache.Ehcache;
 
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
@@ -45,31 +49,31 @@ public class TestCache extends TestJaxrsBase {
     @Test(groups = "slow", description = "Can Invalidate (clear) a Cache by name")
     public void testInvalidateCacheByName() throws Exception {
         // get Ehcache item with name "record-id"
-        final Ehcache cache = cacheManager.getEhcache(CacheType.RECORD_ID.getCacheName());
+        final CacheController<String, Long> cache = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
         // verify that it is not null and has one stored key (the default tenant created for all integration tests)
         assertNotNull(cache);
-        Assert.assertEquals(cache.getSize(), 1);
+        Assert.assertEquals(cache.size(), 1);
 
         // invalidate the specified cache
-        killBillClient.invalidateCacheByName(cache.getName(), requestOptions);
+        killBillClient.invalidateCacheByName(CacheType.RECORD_ID.getCacheName(), requestOptions);
 
         // verify that now the cache is empty and has no keys stored
-        Assert.assertEquals(cache.getSize(), 0);
+        Assert.assertEquals(cache.size(), 0);
     }
 
     @Test(groups = "slow", description = "Can Invalidate (clear) all available Caches")
     public void testInvalidateAllCaches() throws Exception {
         // get Ehcache item with name "record-id"
-        final Ehcache cache = cacheManager.getEhcache(CacheType.RECORD_ID.getCacheName());
+        final CacheController<String, Long> cache = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
         // verify that it is not null and has one stored key (the default tenant created for all integration tests)
         assertNotNull(cache);
-        Assert.assertEquals(cache.getSize(), 1);
+        Assert.assertEquals(cache.size(), 1);
 
         // invalidate all caches
         killBillClient.invalidateAllCaches(requestOptions);
 
         // verify that now the cache is empty and has no keys stored
-        Assert.assertEquals(cache.getSize(), 0);
+        Assert.assertEquals(cache.size(), 0);
     }
 
     @Test(groups = "slow", description = "Can Invalidate (clear) all Account Caches by accountId")
@@ -77,25 +81,23 @@ public class TestCache extends TestJaxrsBase {
         final Account input = createAccountNoPMBundleAndSubscription();
 
         // get all caches per account level
-        final Ehcache accountRecordIdCache = cacheManager.getEhcache(CacheType.ACCOUNT_RECORD_ID.getCacheName());
-        final Ehcache accountImmutableCache = cacheManager.getEhcache(CacheType.ACCOUNT_IMMUTABLE.getCacheName());
-        final Ehcache accountBcdCache = cacheManager.getEhcache(CacheType.ACCOUNT_BCD.getCacheName());
+        final CacheController<String, Long> accountRecordIdCache = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID);
+        final CacheController<Long, ImmutableAccountData> accountImmutableCache = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_IMMUTABLE);
+        final CacheController<UUID, Integer> accountBcdCache = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_BCD);
 
         // verify that they are not null and have the accountId stored as a key (the account created before)
-        assertNotNull(accountRecordIdCache);
-        assertNotNull(accountRecordIdCache.get(input.getAccountId().toString()));
-        assertNotNull(accountImmutableCache);
-        assertNotNull(accountImmutableCache.get(input.getAccountId()));
-        assertNotNull(accountBcdCache);
-        assertNotNull(accountBcdCache.get(input.getAccountId()));
+        assertTrue(accountRecordIdCache.isKeyInCache(input.getAccountId().toString()));
+        final Long accountRecordId = accountRecordIdCache.get(input.getAccountId().toString(), null);
+        assertTrue(accountImmutableCache.isKeyInCache(accountRecordId));
+        assertTrue(accountBcdCache.isKeyInCache(input.getAccountId()));
 
         // invalidate caches per account level by accountId
         killBillClient.invalidateCacheByAccount(input.getAccountId().toString(), requestOptions);
 
         // verify that now the caches don't have the accountId key stored
-        Assert.assertNull(accountRecordIdCache.get(input.getAccountId().toString()));
-        Assert.assertNull(accountImmutableCache.get(input.getAccountId()));
-        Assert.assertNull(accountBcdCache.get(input.getAccountId()));
+        Assert.assertFalse(accountRecordIdCache.isKeyInCache(input.getAccountId().toString()));
+        Assert.assertFalse(accountImmutableCache.isKeyInCache(accountRecordId));
+        Assert.assertFalse(accountBcdCache.isKeyInCache(input.getAccountId()));
     }
 
     @Test(groups = "slow", description = "Can Invalidate (clear) all Tenant Caches for current Tenant")
@@ -125,48 +127,40 @@ public class TestCache extends TestJaxrsBase {
         createAccountWithPMBundleAndSubscriptionAndWaitForFirstInvoiceWithInputOptions(inputOptions);
 
         // get all caches per tenant level
-        final Ehcache tenantRecordIdCache = cacheManager.getEhcache(CacheType.TENANT_RECORD_ID.getCacheName());
-        final Ehcache tenantPaymentStateMachineConfigCache = cacheManager.getEhcache(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG.getCacheName());
-        final Ehcache tenantCache = cacheManager.getEhcache(CacheType.TENANT.getCacheName());
-        final Ehcache tenantKvCache = cacheManager.getEhcache(CacheType.TENANT_KV.getCacheName());
-        final Ehcache tenantConfigCache = cacheManager.getEhcache(CacheType.TENANT_CONFIG.getCacheName());
-        final Ehcache tenantOverdueConfigCache = cacheManager.getEhcache(CacheType.TENANT_OVERDUE_CONFIG.getCacheName());
-        final Ehcache tenantCatalogCache = cacheManager.getEhcache(CacheType.TENANT_CATALOG.getCacheName());
-
-        // getting current Tenant's record Id from the specific Cache
-        Long tenantRecordId = (Long) tenantRecordIdCache.get(currentTenant.getTenantId().toString()).getObjectValue();
+        final CacheController<String, Long> tenantRecordIdCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID);
+        final CacheController<String, StateMachineConfig> tenantPaymentStateMachineConfigCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_PAYMENT_STATE_MACHINE_CONFIG);
+        final CacheController<String, org.killbill.billing.tenant.api.Tenant> tenantCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT);
+        final CacheController<String, String> tenantKvCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_KV);
+        final CacheController<Long, PerTenantConfig> tenantConfigCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_CONFIG);
+        final CacheController<Long, OverdueConfig> tenantOverdueConfigCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_OVERDUE_CONFIG);
+        final CacheController<Long, Catalog> tenantCatalogCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_CATALOG);
 
         // verify that they are not null and have the expected tenant information
-        assertNotNull(tenantRecordIdCache);
-        assertNotNull(tenantRecordIdCache.get(currentTenant.getTenantId().toString()));
-        assertNotNull(tenantPaymentStateMachineConfigCache);
+        assertTrue(tenantRecordIdCache.isKeyInCache(currentTenant.getTenantId().toString()));
+        final Long tenantRecordId = tenantRecordIdCache.get(currentTenant.getTenantId().toString(), null);
+
         assertTrue(hasKeysByTenantRecordId(tenantPaymentStateMachineConfigCache, tenantRecordId.toString()));
-        assertNotNull(tenantCache);
-        assertNotNull(tenantCache.get(testApiKey));
-        assertNotNull(tenantKvCache);
+        assertTrue(tenantCache.isKeyInCache(testApiKey));
         assertTrue(hasKeysByTenantRecordId(tenantKvCache, tenantRecordId.toString()));
-        assertNotNull(tenantConfigCache);
-        assertNotNull(tenantConfigCache.get(tenantRecordId));
-        assertNotNull(tenantOverdueConfigCache);
-        assertNotNull(tenantOverdueConfigCache.get(tenantRecordId));
-        assertNotNull(tenantCatalogCache);
-        assertNotNull(tenantCatalogCache.get(tenantRecordId));
+        assertTrue(tenantConfigCache.isKeyInCache(tenantRecordId));
+        assertTrue(tenantOverdueConfigCache.isKeyInCache(tenantRecordId));
+        assertTrue(tenantCatalogCache.isKeyInCache(tenantRecordId));
 
         // invalidate caches per tenant level
         killBillClient.invalidateCacheByTenant(inputOptions);
 
         // verify that now the caches don't have the previous values
-        Assert.assertNull(tenantRecordIdCache.get(currentTenant.getTenantId().toString()));
+        assertFalse(tenantRecordIdCache.isKeyInCache(currentTenant.getTenantId().toString()));
         assertFalse(hasKeysByTenantRecordId(tenantPaymentStateMachineConfigCache, tenantRecordId.toString()));
-        Assert.assertNull(tenantCache.get(testApiKey));
+        assertFalse(tenantCache.isKeyInCache(testApiKey));
         assertFalse(hasKeysByTenantRecordId(tenantKvCache, tenantRecordId.toString()));
-        Assert.assertNull(tenantConfigCache.get(tenantRecordId));
-        Assert.assertNull(tenantOverdueConfigCache.get(tenantRecordId));
-        Assert.assertNull(tenantCatalogCache.get(tenantRecordId));
+        assertFalse(tenantConfigCache.isKeyInCache(tenantRecordId));
+        assertFalse(tenantOverdueConfigCache.isKeyInCache(tenantRecordId));
+        assertFalse(tenantCatalogCache.isKeyInCache(tenantRecordId));
     }
 
-    private boolean hasKeysByTenantRecordId(final Ehcache tenantCache, final String tenantRecordId) {
-        for (String key : (List<String>) tenantCache.getKeys()) {
+    private boolean hasKeysByTenantRecordId(final CacheController<String, ?> tenantCache, final String tenantRecordId) {
+        for (final String key : tenantCache.getKeys()) {
             if (key.endsWith("::" + tenantRecordId)) {
                 return true;
             }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index bd9c2cf..4427ea6 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -48,6 +48,9 @@ import org.killbill.billing.util.api.AuditLevel;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
@@ -72,7 +75,7 @@ public class TestEntitlement extends TestJaxrsBase {
                                                                ProductCategory.BASE, term, true);
 
         // Retrieves with GET
-        Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+        Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
         Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
         Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
         Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
@@ -95,7 +98,7 @@ public class TestEntitlement extends TestJaxrsBase {
         newInput.setProductCategory(ProductCategory.BASE);
         newInput.setBillingPeriod(entitlementJson.getBillingPeriod());
         newInput.setPriceList(entitlementJson.getPriceList());
-        objFromJson = killBillClient.updateSubscription(newInput, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+        objFromJson = killBillClient.updateSubscription(newInput, CALL_COMPLETION_TIMEOUT_SEC, requestOptions);
         Assert.assertNotNull(objFromJson);
 
         // MOVE AFTER TRIAL
@@ -105,10 +108,10 @@ public class TestEntitlement extends TestJaxrsBase {
         crappyWaitForLackOfProperSynchonization();
 
         // Cancel IMM (Billing EOT)
-        killBillClient.cancelSubscription(newInput.getSubscriptionId(), CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+        killBillClient.cancelSubscription(newInput.getSubscriptionId(), CALL_COMPLETION_TIMEOUT_SEC, requestOptions);
 
         // Retrieves to check EndDate
-        objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+        objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
         assertNotNull(objFromJson.getCancelledDate());
         assertTrue(objFromJson.getCancelledDate().compareTo(new LocalDate(clock.getUTCNow())) == 0);
     }
@@ -166,7 +169,7 @@ public class TestEntitlement extends TestJaxrsBase {
         // Uncancel
         killBillClient.uncancelSubscription(entitlementJson.getSubscriptionId(), requestOptions);
 
-        objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+        objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
         assertNull(objFromJson.getCancelledDate());
         Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
         Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
@@ -188,11 +191,11 @@ public class TestEntitlement extends TestJaxrsBase {
         subscription.setBillingPeriod(BillingPeriod.ANNUAL);
         subscription.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
 
-        assertNull(killBillClient.updateSubscription(subscription, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment));
+        assertNull(killBillClient.updateSubscription(subscription, CALL_COMPLETION_TIMEOUT_SEC, requestOptions));
 
-        killBillClient.cancelSubscription(subscriptionId, createdBy, reason, comment);
+        killBillClient.cancelSubscription(subscriptionId, requestOptions);
 
-        assertNull(killBillClient.getSubscription(subscriptionId));
+        assertNull(killBillClient.getSubscription(subscriptionId, requestOptions));
     }
 
     @Test(groups = "slow", description = "Can override billing policy on change")
@@ -209,7 +212,7 @@ public class TestEntitlement extends TestJaxrsBase {
                                                                 ProductCategory.BASE, term, true);
 
         // Retrieves with GET
-        Subscription objFromJson = killBillClient.getSubscription(subscriptionJson.getSubscriptionId());
+        Subscription objFromJson = killBillClient.getSubscription(subscriptionJson.getSubscriptionId(), requestOptions);
         // Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
         objFromJson.setPriceOverrides(null);
         subscriptionJson.setPriceOverrides(null);
@@ -225,7 +228,7 @@ public class TestEntitlement extends TestJaxrsBase {
         newInput.setProductCategory(ProductCategory.BASE);
         newInput.setBillingPeriod(BillingPeriod.MONTHLY);
         newInput.setPriceList(subscriptionJson.getPriceList());
-        objFromJson = killBillClient.updateSubscription(newInput, BillingActionPolicy.IMMEDIATE, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+        objFromJson = killBillClient.updateSubscription(newInput, BillingActionPolicy.IMMEDIATE, CALL_COMPLETION_TIMEOUT_SEC, requestOptions );
         Assert.assertNotNull(objFromJson);
         assertEquals(objFromJson.getBillingPeriod(), BillingPeriod.MONTHLY);
     }
@@ -354,15 +357,17 @@ public class TestEntitlement extends TestJaxrsBase {
         subscriptions.add(base);
         subscriptions.add(addOn1);
         subscriptions.add(addOn2);
-        final Bundle bundle = killBillClient.createSubscriptionWithAddOns(subscriptions, null, 10, "createdBy", "", "");
+        final Bundle bundle = killBillClient.createSubscriptionWithAddOns(subscriptions, null, 10, requestOptions);
         assertNotNull(bundle);
         assertEquals(bundle.getExternalKey(), "base");
         assertEquals(bundle.getSubscriptions().size(), 3);
 
-        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL);
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL, requestOptions);
         assertEquals(invoices.size(), 1);
     }
 
+
+
     @Test(groups = "slow", description = "Create a bulk of base entitlement and addOns under the same transaction")
     public void testCreateEntitlementsWithAddOns() throws Exception {
         final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
@@ -411,13 +416,18 @@ public class TestEntitlement extends TestJaxrsBase {
     }
 
     @Test(groups = "slow", description = "Create a bulk of base entitlements and addOns under the same transaction",
-            expectedExceptions = KillBillClientException.class, expectedExceptionsMessageRegExp = "SubscriptionJson Base Entitlement needs to be provided")
+            expectedExceptions = KillBillClientException.class, expectedExceptionsMessageRegExp = "Missing Base Subscription for bundle 12345")
     public void testCreateEntitlementsWithoutBase() throws Exception {
         final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
         clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
 
         final Account accountJson = createAccountWithDefaultPaymentMethod();
 
+        final Subscription bp = new Subscription();
+        bp.setAccountId(accountJson.getAccountId());
+        bp.setProductCategory(ProductCategory.BASE);
+        bp.setExternalKey("12345");
+
         final Subscription addOn1 = new Subscription();
         addOn1.setAccountId(accountJson.getAccountId());
         addOn1.setProductName("Telescopic-Scope");
@@ -426,6 +436,7 @@ public class TestEntitlement extends TestJaxrsBase {
         addOn1.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
 
         final List<Subscription> subscriptions = new ArrayList<Subscription>();
+        subscriptions.add(bp);
         subscriptions.add(addOn1);
 
         final List<BulkBaseSubscriptionAndAddOns> bulkList = new ArrayList<BulkBaseSubscriptionAndAddOns>();
@@ -435,6 +446,58 @@ public class TestEntitlement extends TestJaxrsBase {
         killBillClient.createSubscriptionsWithAddOns(bulkList, null, 10, requestOptions);
     }
 
+    @Test(groups = "slow", description = "Create addOns in a bundle where BP subscrsiptions already exist")
+    public void testEntitlementsWithAddOnsAndAlreadyExistingBP() throws Exception {
+        final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
+        clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());
+
+        final Account accountJson = createAccountWithDefaultPaymentMethod();
+
+        final Subscription input = new Subscription();
+        input.setAccountId(accountJson.getAccountId());
+        input.setExternalKey("foobarxyz");
+        input.setProductName("Shotgun");
+        input.setProductCategory(ProductCategory.BASE);
+        input.setBillingPeriod(BillingPeriod.MONTHLY);
+        input.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+        final Subscription subscription = killBillClient.createSubscription(input, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, requestOptions);
+
+        final Subscription base = new Subscription();
+        base.setAccountId(accountJson.getAccountId());
+        base.setProductCategory(ProductCategory.BASE);
+        base.setExternalKey("foobarxyz");
+
+        final Subscription addOn1 = new Subscription();
+        addOn1.setAccountId(accountJson.getAccountId());
+        addOn1.setProductName("Telescopic-Scope");
+        addOn1.setProductCategory(ProductCategory.ADD_ON);
+        addOn1.setBillingPeriod(BillingPeriod.MONTHLY);
+        addOn1.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+        final Subscription addOn2 = new Subscription();
+        addOn2.setAccountId(accountJson.getAccountId());
+        addOn2.setProductName("Laser-Scope");
+        addOn2.setProductCategory(ProductCategory.ADD_ON);
+        addOn2.setBillingPeriod(BillingPeriod.MONTHLY);
+        addOn2.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+        final List<Subscription> subscriptions = new ArrayList<Subscription>();
+        subscriptions.add(base);
+        subscriptions.add(addOn1);
+        subscriptions.add(addOn2);
+
+        final Iterable<BulkBaseSubscriptionAndAddOns> bulkBaseSubscriptionAndAddOns = ImmutableList.of(new BulkBaseSubscriptionAndAddOns(subscriptions));
+
+        final Bundles bundles = killBillClient.createSubscriptionsWithAddOns(bulkBaseSubscriptionAndAddOns, null, 10, requestOptions);
+        assertNotNull(bundles);
+        assertEquals(bundles.size(), 1);
+        assertEquals(bundles.get(0).getSubscriptions().size(), 3);
+
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL, requestOptions);
+        assertEquals(invoices.size(), 2);
+    }
+
+
     @Test(groups = "slow", description = "Can create an entitlement in the future")
     public void testCreateEntitlementInTheFuture() throws Exception {
         final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
@@ -518,7 +581,7 @@ public class TestEntitlement extends TestJaxrsBase {
         killBillClient.updateSubscriptionBCD(updatedSubscription, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, requestOptions);
 
 
-        final Subscription result = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+        final Subscription result = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
         // Still shows as the 4 (BCD did not take effect)
         Assert.assertEquals(result.getBillCycleDayLocal(), new Integer(25));
 
@@ -526,7 +589,7 @@ public class TestEntitlement extends TestJaxrsBase {
         clock.addDays(14);
         crappyWaitForLackOfProperSynchonization();
 
-        final Subscription result2 = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+        final Subscription result2 = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
         // Still shows as the 4 (BCD did not take effect)
         Assert.assertEquals(result2.getBillCycleDayLocal(), new Integer(9));
     }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
index d4ec7bf..66007ef 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -74,7 +74,6 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.inject.Module;
 import com.google.inject.util.Modules;
-import net.sf.ehcache.CacheManager;
 
 public class TestJaxrsBase extends KillbillClient {
 
@@ -107,9 +106,6 @@ public class TestJaxrsBase extends KillbillClient {
     @Inject
     protected TenantCacheInvalidation tenantCacheInvalidation;
 
-    @Inject
-    protected CacheManager cacheManager;
-
     protected DaoConfig daoConfig;
     protected KillbillServerConfig serverConfig;
 
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
index c50ae34..934b81e 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestPatternObfuscator.java
@@ -181,6 +181,10 @@ public class TestPatternObfuscator extends ServerTestSuiteNoDB {
                "    \"id\": \"card_483etw4er9fg4vF3sQdrt3FG\",\n" +
                "    \"object\": \"card\",\n" +
                "    \"banknumber\": 4111111111111111,\n" +
+               "    \"cvv\" : 111,\n" +
+               "    \"cvv\": 111,\n" +
+               "    \"cvv\": \"111\",\n" +
+               "    \"data\": {\"cvv\" : 111 },\n" +
                "    \"last4\": \"0000\",\n" +
                "    \"brand\": \"Visa\",\n" +
                "    \"funding\": \"credit\",\n" +
@@ -204,6 +208,10 @@ public class TestPatternObfuscator extends ServerTestSuiteNoDB {
                "    \"id\": \"card_483etw4er9fg4vF3sQdrt3FG\",\n" +
                "    \"object\": \"card\",\n" +
                "    \"banknumber\": ****************,\n" +
+               "    \"cvv\" : ***,\n" +
+               "    \"cvv\": ***,\n" +
+               "    \"cvv\": *****,\n" +
+               "    \"data\": {\"cvv\" : ****},\n" +
                "    \"last4\": \"0000\",\n" +
                "    \"brand\": \"Visa\",\n" +
                "    \"funding\": \"credit\",\n" +
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index d6cd1b3..1b05a82 100644
--- a/profiles/killpay/pom.xml
+++ b/profiles/killpay/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <artifactId>killbill-profiles</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles-killpay</artifactId>

profiles/pom.xml 2(+1 -1)

diff --git a/profiles/pom.xml b/profiles/pom.xml
index 1e1e897..1be89b4 100644
--- a/profiles/pom.xml
+++ b/profiles/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-profiles</artifactId>
diff --git a/subscription/pom.xml b/subscription/pom.xml
index c01bb38..f878deb 100644
--- a/subscription/pom.xml
+++ b/subscription/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-subscription</artifactId>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index ccc67eb..d3bdedf 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -54,7 +55,6 @@ import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
 import org.killbill.billing.entitlement.api.EntitlementSpecifier;
-import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.subscription.api.SubscriptionApiBase;
@@ -65,9 +65,7 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
 import org.killbill.billing.subscription.api.SubscriptionBaseWithAddOns;
 import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
-import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseApiService;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle;
-import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseWithAddOns;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionStatusDryRun;
 import org.killbill.billing.subscription.api.user.SubscriptionAndAddOnsSpecifier;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
@@ -207,12 +205,15 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
 
     private List<SubscriptionSpecifier> verifyAndBuildSubscriptionSpecifiers(final UUID bundleId, final String externalKey, final Iterable<EntitlementSpecifier> entitlements, final boolean isMigrated, final InternalCallContext context, final DateTime now, final DateTime effectiveDate, final Catalog catalog, final CallContext callContext) throws SubscriptionBaseApiException, CatalogApiException {
         final List<SubscriptionSpecifier> subscriptions = new ArrayList<SubscriptionSpecifier>();
-        boolean first = true;
         final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundleId, null, context);
 
         for (final EntitlementSpecifier entitlement : entitlements) {
 
             final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+            if (spec == null) {
+                // BP already exists
+                continue;
+            }
 
             final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
 
@@ -223,12 +224,6 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                               spec.getProductName(), spec.getBillingPeriod().toString(), plan.getPriceListName()));
             }
 
-            if (first) {
-                first = false;
-                if (plan.getProduct().getCategory() != ProductCategory.BASE) {
-                    throw new SubscriptionBaseApiException(new IllegalArgumentException(), ErrorCode.SUB_CREATE_NO_BP.getCode(), "Missing Base Subscription.");
-                }
-            }
 
             // verify the number of subscriptions (of the same kind) allowed per bundle and the existing ones
             if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
@@ -262,6 +257,37 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         return subscriptions;
     }
 
+    private boolean sanityAndReorderBPSpecFirst(final Catalog catalog, final BaseEntitlementWithAddOnsSpecifier entitlementWithAddOnsSpecifier, final DateTime effectiveDate, final List<EntitlementSpecifier> outputEntitlementSpecifier) throws SubscriptionBaseApiException {
+
+
+        EntitlementSpecifier basePlanSpecifier = null;
+        final List<EntitlementSpecifier> addOnSpecifiers = new ArrayList<EntitlementSpecifier>();
+        try {
+            for (final EntitlementSpecifier cur : entitlementWithAddOnsSpecifier.getEntitlementSpecifier()) {
+                final Plan inputPlan = catalog.createOrFindPlan(cur.getPlanPhaseSpecifier(), null, effectiveDate);
+                final boolean isBaseSpecifier = inputPlan.getProduct().getCategory() == ProductCategory.BASE;
+                if (isBaseSpecifier) {
+                    if (basePlanSpecifier == null) {
+                        basePlanSpecifier =  cur;
+                    } else {
+                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+                    }
+                } else {
+                    addOnSpecifiers.add(cur);
+                }
+            }
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+
+        if (basePlanSpecifier != null) {
+            outputEntitlementSpecifier.add(basePlanSpecifier);
+        }
+        outputEntitlementSpecifier.addAll(addOnSpecifiers);
+        return basePlanSpecifier != null;
+    }
+
+
     @Override
     public List<SubscriptionBaseWithAddOns> createBaseSubscriptionsWithAddOns(final UUID accountId, final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifier, final InternalCallContext context) throws SubscriptionBaseApiException {
         try {
@@ -274,14 +300,32 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                 final DateTime effectiveDate = (entitlementWithAddOnsSpecifier.getBillingEffectiveDate() != null) ?
                                                DefaultClock.truncateMs(entitlementWithAddOnsSpecifier.getBillingEffectiveDate().toDateTimeAtStartOfDay()) : now;
 
-                final SubscriptionBaseBundle bundle = createBundleForAccount(accountId, entitlementWithAddOnsSpecifier.getExternalKey(), context);
+
+
+                final List<EntitlementSpecifier> reorderedSpecifiers = new ArrayList<EntitlementSpecifier>();
+                final boolean isBaseSpecifierExists = sanityAndReorderBPSpecFirst(catalog, entitlementWithAddOnsSpecifier, effectiveDate, reorderedSpecifiers);
+
+                final SubscriptionBaseBundle bundle;
+                if (isBaseSpecifierExists) {
+                    bundle = createBundleForAccount(accountId, entitlementWithAddOnsSpecifier.getExternalKey(), context);
+                } else {
+                    final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(entitlementWithAddOnsSpecifier.getExternalKey(), context);
+                    final SubscriptionBaseBundle tmp = getActiveBundleForKeyNotException(existingBundles, dao, clock, context);
+                    if (tmp == null) {
+                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, entitlementWithAddOnsSpecifier.getExternalKey());
+                    } else if (!tmp.getAccountId().equals(accountId)) {
+                        throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_ACTIVE_BUNDLE_KEY_EXISTS, entitlementWithAddOnsSpecifier.getExternalKey());
+                    } else {
+                        bundle = tmp;
+                    }
+                }
 
                 final SubscriptionAndAddOnsSpecifier subscriptionAndAddOnsSpecifier = new SubscriptionAndAddOnsSpecifier(
                         bundle.getId(),
                         effectiveDate,
                         verifyAndBuildSubscriptionSpecifiers(bundle.getId(),
                                                              bundle.getExternalKey(),
-                                                             entitlementWithAddOnsSpecifier.getEntitlementSpecifier(),
+                                                             reorderedSpecifiers,
                                                              entitlementWithAddOnsSpecifier.isMigrated(),
                                                              context,
                                                              now,
@@ -336,6 +380,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
                                                 context);
     }
 
+
     @Override
     public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
 
@@ -823,6 +868,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
         return requestedDate == null ? clock.getUTCNow() : internalCallContext.toUTCDateTime(requestedDate);
     }
 
+
     private DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final DefaultSubscriptionBase baseSubscription, final Plan plan,
                                                   final DateTime effectiveDate, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException {
         switch (plan.getProduct().getCategory()) {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 77b73c9..f6dac4c 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -143,11 +143,17 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         final InternalCallContext internalCallContext = createCallContextFromAccountId(accountId, context);
         dao.createSubscriptionsWithAddOns(allSubscriptions, eventsMap, internalCallContext);
 
-        for (final List<SubscriptionBase> subscriptions : subscriptionBaseAndAddOnsList) {
-            final SubscriptionBase baseSubscription = findBaseSubscription(subscriptions);
-            rebuildTransitions(internalCallContext, subscriptions, baseSubscription);
+        try {
+            for (final List<SubscriptionBase> subscriptions : subscriptionBaseAndAddOnsList) {
+                for (final SubscriptionBase input : subscriptions) {
+                    ((DefaultSubscriptionBase) input).rebuildTransitions(dao.getEventsForSubscription(input.getId(), internalCallContext),
+                                                                         catalogService.getFullCatalog(true, true, internalCallContext));
+                }
+            }
+            return allSubscriptions;
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
         }
-        return allSubscriptions;
     }
 
     private void createEvents(final Iterable<SubscriptionSpecifier> subscriptions, final CallContext context, final Map<UUID, List<SubscriptionBaseEvent>> eventsMap, final Collection<SubscriptionBase> subscriptionBaseList) throws SubscriptionBaseApiException {
@@ -167,35 +173,6 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         }
     }
 
-    private void rebuildTransitions(final InternalCallContext internalCallContext, final Iterable<SubscriptionBase> subscriptions, final SubscriptionBase baseSubscription) throws SubscriptionBaseApiException {
-        try {
-            // Safe cast
-            ((DefaultSubscriptionBase) baseSubscription).rebuildTransitions(dao.getEventsForSubscription(baseSubscription.getId(), internalCallContext),
-                                                                            catalogService.getFullCatalog(true, true, internalCallContext));
-
-            for (final SubscriptionBase input : subscriptions) {
-                if (input.getId().equals(baseSubscription.getId())) {
-                    continue;
-                }
-
-                // Safe cast
-                ((DefaultSubscriptionBase) input).rebuildTransitions(dao.getEventsForSubscription(input.getId(), internalCallContext),
-                                                                     catalogService.getFullCatalog(true, true, internalCallContext));
-            }
-        } catch (final CatalogApiException e) {
-            throw new SubscriptionBaseApiException(e);
-        }
-    }
-
-    private SubscriptionBase findBaseSubscription(final Iterable<SubscriptionBase> subscriptionBaseList) {
-        return Iterables.tryFind(subscriptionBaseList, new Predicate<SubscriptionBase>() {
-            @Override
-            public boolean apply(final SubscriptionBase subscription) {
-                return ProductCategory.BASE.equals(subscription.getCategory());
-            }
-        }).orNull();
-    }
-
     @Override
     public boolean cancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException {
         final EntitlementState currentState = subscription.getState();
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index af140fb..332d497 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -258,10 +258,9 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<SubscriptionBaseBundle>() {
             @Override
             public SubscriptionBaseBundle inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws EntityPersistenceException {
-
                 final SubscriptionBundleModelDao model = new SubscriptionBundleModelDao(bundle);
-                entitySqlDaoWrapperFactory.become(BundleSqlDao.class).create(model, context);
-                final SubscriptionBundleModelDao result = entitySqlDaoWrapperFactory.become(BundleSqlDao.class).getById(bundle.getId().toString(), context);
+                final BundleSqlDao bundleSqlDao = entitySqlDaoWrapperFactory.become(BundleSqlDao.class);
+                final SubscriptionBundleModelDao result = createAndRefresh(bundleSqlDao, model, context);
                 return SubscriptionBundleModelDao.toSubscriptionbundle(result);
             }
         });
@@ -425,7 +424,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                 final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
                 final UUID subscriptionId = subscription.getId();
                 cancelNextPhaseEventFromTransaction(subscriptionId, entitySqlDaoWrapperFactory, context);
-                transactional.create(new SubscriptionEventModelDao(nextPhaseEvent), context);
+                createAndRefresh(transactional, new SubscriptionEventModelDao(nextPhaseEvent), context);
                 recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory,
                                                         nextPhaseEvent.getEffectiveDate(),
                                                         new SubscriptionNotificationKey(nextPhaseEvent.getId()), context);
@@ -503,11 +502,11 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final SubscriptionSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class);
-                transactional.create(new SubscriptionModelDao(subscription), context);
+                createAndRefresh(transactional, new SubscriptionModelDao(subscription), context);
 
                 final SubscriptionEventSqlDao eventsDaoFromSameTransaction = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
                 for (final SubscriptionBaseEvent cur : initialEvents) {
-                    eventsDaoFromSameTransaction.create(new SubscriptionEventModelDao(cur), context);
+                    createAndRefresh(eventsDaoFromSameTransaction, new SubscriptionEventModelDao(cur), context);
 
                     final boolean isBusEvent = cur.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 && (cur.getType() == EventType.API_USER);
                     recordBusOrFutureNotificationFromTransaction(subscription, cur, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
@@ -534,12 +533,11 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                     for (final SubscriptionBase subscriptionBase : subscription.getSubscriptionBaseList()) {
                         // Safe cast
                         final DefaultSubscriptionBase defaultSubscriptionBase = (DefaultSubscriptionBase) subscriptionBase;
-
-                        transactional.create(new SubscriptionModelDao(defaultSubscriptionBase), context);
+                        createAndRefresh(transactional, new SubscriptionModelDao(defaultSubscriptionBase), context);
 
                         final List<SubscriptionBaseEvent> initialEvents = initialEventsMap.get(defaultSubscriptionBase.getId());
                         for (final SubscriptionBaseEvent cur : initialEvents) {
-                            eventsDaoFromSameTransaction.create(new SubscriptionEventModelDao(cur), context);
+                            createAndRefresh(eventsDaoFromSameTransaction, new SubscriptionEventModelDao(cur), context);
 
                             final boolean isBusEvent = cur.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 && (cur.getType() == EventType.API_USER);
                             recordBusOrFutureNotificationFromTransaction(defaultSubscriptionBase, cur, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
@@ -654,7 +652,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
                 cancelFutureEventsFromTransaction(subscriptionId, changeEvents.get(0).getEffectiveDate(), entitySqlDaoWrapperFactory, false, context);
 
                 for (final SubscriptionBaseEvent cur : changeEvents) {
-                    transactional.create(new SubscriptionEventModelDao(cur), context);
+                    createAndRefresh(transactional, new SubscriptionEventModelDao(cur), context);
 
                     final boolean isBusEvent = cur.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 && (cur.getType() == EventType.API_USER);
                     recordBusOrFutureNotificationFromTransaction(subscription, cur, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
@@ -701,7 +699,8 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
             throws EntityPersistenceException {
         final UUID subscriptionId = subscription.getId();
         cancelFutureEventsFromTransaction(subscriptionId, cancelEvent.getEffectiveDate(), entitySqlDaoWrapperFactory, true, context);
-        entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class).create(new SubscriptionEventModelDao(cancelEvent), context);
+        final SubscriptionEventSqlDao subscriptionEventSqlDao = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
+        createAndRefresh(subscriptionEventSqlDao, new SubscriptionEventModelDao(cancelEvent), context);
 
         final boolean isBusEvent = cancelEvent.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0;
         recordBusOrFutureNotificationFromTransaction(subscription, cancelEvent, entitySqlDaoWrapperFactory, isBusEvent, seqId, context);
@@ -957,7 +956,7 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
             @Override
             public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final SubscriptionEventSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
-                transactional.create(new SubscriptionEventModelDao(bcdEvent), context);
+                createAndRefresh(transactional, new SubscriptionEventModelDao(bcdEvent), context);
 
                 // Notify the Bus
                 notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, bcdEvent, SubscriptionBaseTransitionType.BCD_CHANGE, context);
@@ -1072,20 +1071,20 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
         for (final SubscriptionTransferData curSubscription : bundleTransferData.getSubscriptions()) {
             final DefaultSubscriptionBase subData = curSubscription.getData();
             for (final SubscriptionBaseEvent curEvent : curSubscription.getInitialEvents()) {
-                transactional.create(new SubscriptionEventModelDao(curEvent), context);
+                createAndRefresh(transactional, new SubscriptionEventModelDao(curEvent), context);
                 recordFutureNotificationFromTransaction(entitySqlDaoWrapperFactory,
                                                         curEvent.getEffectiveDate(),
                                                         new SubscriptionNotificationKey(curEvent.getId()),
                                                         context);
             }
-            transSubDao.create(new SubscriptionModelDao(subData), context);
+            createAndRefresh(transSubDao, new SubscriptionModelDao(subData), context);
 
             // Notify the Bus of the latest requested change
             final SubscriptionBaseEvent finalEvent = curSubscription.getInitialEvents().get(curSubscription.getInitialEvents().size() - 1);
             notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subData, finalEvent, SubscriptionBaseTransitionType.TRANSFER, context);
         }
 
-        transBundleDao.create(new SubscriptionBundleModelDao(bundleData), context);
+        createAndRefresh(transBundleDao, new SubscriptionBundleModelDao(bundleData), context);
     }
 
     //
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
index cd663dd..968376a 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/api/user/TestUserApiError.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -77,6 +77,9 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateSubscriptionAddOnNotAvailable() throws SubscriptionBaseApiException {
         final SubscriptionBaseBundle aoBundle = subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), "myAOBundle", internalCallContext);
+        mockNonEntityDao.addTenantRecordIdMapping(aoBundle.getId(), internalCallContext);
+        mockNonEntityDao.addAccountRecordIdMapping(aoBundle.getId(), internalCallContext);
+
         testUtil.createSubscriptionWithBundle(aoBundle.getId(), "Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
         tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE);
     }
@@ -84,6 +87,9 @@ public class TestUserApiError extends SubscriptionTestSuiteNoDB {
     @Test(groups = "fast")
     public void testCreateSubscriptionAddOnIncluded() throws SubscriptionBaseApiException {
         final SubscriptionBaseBundle aoBundle = subscriptionInternalApi.createBundleForAccount(bundle.getAccountId(), "myAOBundle", internalCallContext);
+        mockNonEntityDao.addTenantRecordIdMapping(aoBundle.getId(), internalCallContext);
+        mockNonEntityDao.addAccountRecordIdMapping(aoBundle.getId(), internalCallContext);
+
         testUtil.createSubscriptionWithBundle(aoBundle.getId(), "Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
         tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.SUB_CREATE_AO_ALREADY_INCLUDED);
     }
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java
index 0ace9f9..b90587c 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/SubscriptionTestSuiteNoDB.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -24,7 +24,10 @@ import javax.inject.Inject;
 
 import org.killbill.billing.GuicyKillbillTestSuiteNoDB;
 import org.killbill.billing.account.api.AccountData;
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.api.TestApiListener;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogService;
 import org.killbill.billing.dao.MockNonEntityDao;
@@ -62,6 +65,8 @@ public class SubscriptionTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     protected static final Logger log = LoggerFactory.getLogger(SubscriptionTestSuiteNoDB.class);
 
     @Inject
+    protected ImmutableAccountInternalApi immutableAccountInternalApi;
+    @Inject
     protected SubscriptionBaseService subscriptionBaseService;
     @Inject
     protected SubscriptionBaseInternalApi subscriptionInternalApi;
@@ -132,7 +137,13 @@ public class SubscriptionTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
         this.accountData = subscriptionTestInitializer.initAccountData();
         final UUID accountId = UUIDs.randomUUID();
         mockNonEntityDao.addTenantRecordIdMapping(accountId, internalCallContext);
+
+        final ImmutableAccountData immutableAccountData = Mockito.mock(ImmutableAccountData.class);
+        Mockito.when(immutableAccountInternalApi.getImmutableAccountDataByRecordId(Mockito.<Long>eq(internalCallContext.getAccountRecordId()), Mockito.<InternalTenantContext>any())).thenReturn(immutableAccountData);
+
         this.bundle = subscriptionTestInitializer.initBundle(accountId, subscriptionInternalApi, internalCallContext);
+        mockNonEntityDao.addTenantRecordIdMapping(bundle.getId(), internalCallContext);
+        mockNonEntityDao.addAccountRecordIdMapping(bundle.getId(), internalCallContext);
     }
 
     @AfterMethod(groups = "fast")

tenant/pom.xml 2(+1 -1)

diff --git a/tenant/pom.xml b/tenant/pom.xml
index b7db50f..c866d9f 100644
--- a/tenant/pom.xml
+++ b/tenant/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-tenant</artifactId>
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java
index a7f56b6..dbda74f 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenant.java
@@ -16,22 +16,35 @@
 
 package org.killbill.billing.tenant.api;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
-
 import org.killbill.billing.tenant.dao.TenantModelDao;
-import org.killbill.billing.entity.EntityBase;
 import org.killbill.billing.util.UUIDs;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
+
+public class DefaultTenant implements Tenant, Externalizable {
 
-public class DefaultTenant extends EntityBase implements Tenant {
+    private static final long serialVersionUID = -6662488328218280007L;
 
-    private final String externalKey;
-    private final String apiKey;
+    private UUID id;
+    private DateTime createdDate;
+    private DateTime updatedDate;
+    private String externalKey;
+    private String apiKey;
     // Decrypted (clear) secret
-    private final String apiSecret;
+    private transient String apiSecret;
+
+    // For deserialization
+    public DefaultTenant() {}
 
     /**
      * This call is used to create a tenant
@@ -54,7 +67,9 @@ public class DefaultTenant extends EntityBase implements Tenant {
 
     public DefaultTenant(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
                          final String externalKey, final String apiKey, final String apiSecret) {
-        super(id, createdDate, updatedDate);
+        this.id = id;
+        this.createdDate = createdDate;
+        this.updatedDate = updatedDate;
         this.externalKey = externalKey;
         this.apiKey = apiKey;
         this.apiSecret = apiSecret;
@@ -66,6 +81,21 @@ public class DefaultTenant extends EntityBase implements Tenant {
     }
 
     @Override
+    public UUID getId() {
+        return id;
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    @Override
+    public DateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
+    @Override
     public String getExternalKey() {
         return externalKey;
     }
@@ -82,9 +112,11 @@ public class DefaultTenant extends EntityBase implements Tenant {
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("DefaultTenant");
-        sb.append("{externalKey='").append(externalKey).append('\'');
+        final StringBuilder sb = new StringBuilder("DefaultTenant{");
+        sb.append("id=").append(id);
+        sb.append(", createdDate=").append(createdDate);
+        sb.append(", updatedDate=").append(updatedDate);
+        sb.append(", externalKey='").append(externalKey).append('\'');
         sb.append(", apiKey='").append(apiKey).append('\'');
         // Don't print the secret
         sb.append('}');
@@ -102,24 +134,42 @@ public class DefaultTenant extends EntityBase implements Tenant {
 
         final DefaultTenant that = (DefaultTenant) o;
 
-        if (apiKey != null ? !apiKey.equals(that.apiKey) : that.apiKey != null) {
+        if (id != null ? !id.equals(that.id) : that.id != null) {
+            return false;
+        }
+        if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+            return false;
+        }
+        if (updatedDate != null ? !updatedDate.equals(that.updatedDate) : that.updatedDate != null) {
             return false;
         }
         if (externalKey != null ? !externalKey.equals(that.externalKey) : that.externalKey != null) {
             return false;
         }
-        if (apiSecret != null ? !apiSecret.equals(that.apiSecret) : that.apiSecret != null) {
+        if (apiKey != null ? !apiKey.equals(that.apiKey) : that.apiKey != null) {
             return false;
         }
-
-        return true;
+        return apiSecret != null ? apiSecret.equals(that.apiSecret) : that.apiSecret == null;
     }
 
     @Override
     public int hashCode() {
-        int result = externalKey != null ? externalKey.hashCode() : 0;
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        result = 31 * result + (updatedDate != null ? updatedDate.hashCode() : 0);
+        result = 31 * result + (externalKey != null ? externalKey.hashCode() : 0);
         result = 31 * result + (apiKey != null ? apiKey.hashCode() : 0);
         result = 31 * result + (apiSecret != null ? apiSecret.hashCode() : 0);
         return result;
     }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidationCallback.java b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidationCallback.java
index f07122f..ffc662c 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidationCallback.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/TenantCacheInvalidationCallback.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -32,7 +32,7 @@ public class TenantCacheInvalidationCallback implements CacheInvalidationCallbac
 
     private final Logger log = LoggerFactory.getLogger(TenantCacheInvalidationCallback.class);
 
-    private final CacheController<Object, Object> tenantKVCache;
+    private final CacheController<String, String> tenantKVCache;
 
     @Inject
     public TenantCacheInvalidationCallback(final CacheControllerDispatcher cacheControllerDispatcher) {
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
index b9a22bc..eb3f5ba 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/user/DefaultTenantUserApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -71,8 +71,8 @@ public class DefaultTenantUserApi implements TenantUserApi {
 
     private final TenantDao tenantDao;
     private final InternalCallContextFactory internalCallContextFactory;
-    private final CacheController<Object, Object> tenantKVCache;
-    private final CacheController<Object, Object> tenantCache;
+    private final CacheController<String, String> tenantKVCache;
+    private final CacheController<String, Tenant> tenantCache;
 
 
     @Inject
@@ -101,7 +101,7 @@ public class DefaultTenantUserApi implements TenantUserApi {
 
     @Override
     public Tenant getTenantByApiKey(final String key) throws TenantApiException {
-        final Tenant tenant = (Tenant) tenantCache.get(key, new CacheLoaderArgument(ObjectType.TENANT));
+        final Tenant tenant = tenantCache.get(key, new CacheLoaderArgument(ObjectType.TENANT));
         if (tenant == null) {
             throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_API_KEY, key);
         }
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
index 81477ce..18f2238 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -98,7 +98,8 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
                 final TenantModelDao tenantModelDaoWithSecret = new TenantModelDao(entity.getId(), context.getCreatedDate(), context.getUpdatedDate(),
                                                                                    entity.getExternalKey(), entity.getApiKey(),
                                                                                    hashedPasswordBase64, salt.toBase64());
-                entitySqlDaoWrapperFactory.become(TenantSqlDao.class).create(tenantModelDaoWithSecret, context);
+                final TenantSqlDao tenantSqlDao = entitySqlDaoWrapperFactory.become(TenantSqlDao.class);
+                createAndRefresh(tenantSqlDao, tenantModelDaoWithSecret, context);
                 return null;
             }
         });
@@ -145,8 +146,7 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
                 if (uniqueKey) {
                     deleteFromTransaction(key, entitySqlDaoWrapperFactory, context);
                 }
-                tenantKVSqlDao.create(tenantKVModelDao, context);
-                final TenantKVModelDao rehydrated = tenantKVSqlDao.getById(tenantKVModelDao.getId().toString(), context);
+                final TenantKVModelDao rehydrated = createAndRefresh(tenantKVSqlDao, tenantKVModelDao, context);
                 broadcastConfigurationChangeFromTransaction(rehydrated.getRecordId(), key, entitySqlDaoWrapperFactory, context);
                 return null;
             }
@@ -164,14 +164,14 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
                 // Retrieve all values for key ordered with recordId (last at the end)
                 final List<TenantKVModelDao> tenantKV = tenantKVSqlDao.getTenantValueForKey(key, context);
                 final String id;
+                final TenantKVModelDao rehydrated;
                 if (!tenantKV.isEmpty()) {
                     id = tenantKV.get(tenantKV.size() - 1).getId().toString();
-                    tenantKVSqlDao.updateTenantValueKey(id, value, context);
+                    rehydrated = (TenantKVModelDao) tenantKVSqlDao.updateTenantValueKey(id, value, context);
                 } else {
                     id = tenantKVModelDao.getId().toString();
-                    tenantKVSqlDao.create(tenantKVModelDao, context);
+                    rehydrated = createAndRefresh(tenantKVSqlDao, tenantKVModelDao, context);
                 }
-                final TenantKVModelDao rehydrated = tenantKVSqlDao.getById(id, context);
                 broadcastConfigurationChangeFromTransaction(rehydrated.getRecordId(), key, entitySqlDaoWrapperFactory, context);
                 return null;
             }
@@ -225,7 +225,8 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
                                                              final InternalCallContext context) throws EntityPersistenceException {
         if (isSystemKey(key)) {
             final TenantBroadcastModelDao broadcast = new TenantBroadcastModelDao(kvRecordId, key, context.getUserToken());
-            entitySqlDaoWrapperFactory.become(TenantBroadcastSqlDao.class).create(broadcast, context);
+            final TenantBroadcastSqlDao tenantBroadcastSqlDao = entitySqlDaoWrapperFactory.become(TenantBroadcastSqlDao.class);
+            createAndRefresh(tenantBroadcastSqlDao, broadcast, context);
         }
     }
 
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java
index fd523cb..c0cac34 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantKVSqlDao.java
@@ -49,9 +49,7 @@ public interface TenantKVSqlDao extends EntitySqlDao<TenantKVModelDao, TenantKV>
 
     @SqlUpdate
     @Audited(ChangeType.UPDATE)
-    public void updateTenantValueKey(@Bind("id") final String id,
-                                     @Bind("tenantValue") final String tenantValue,
-                                     @BindBean final InternalCallContext context);
-
-
+    public Object updateTenantValueKey(@Bind("id") final String id,
+                                       @Bind("tenantValue") final String tenantValue,
+                                       @BindBean final InternalCallContext context);
 }

usage/pom.xml 2(+1 -1)

diff --git a/usage/pom.xml b/usage/pom.xml
index 345bc3c..d500534 100644
--- a/usage/pom.xml
+++ b/usage/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-usage</artifactId>

util/pom.xml 31(+22 -9)

diff --git a/util/pom.xml b/util/pom.xml
index 7743895..9d9fb0a 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.18.5-SNAPSHOT</version>
+        <version>0.18.7-SNAPSHOT</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <artifactId>killbill-util</artifactId>
@@ -50,6 +50,10 @@
             <artifactId>jackson-dataformat-csv</artifactId>
         </dependency>
         <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-smile</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.fasterxml.jackson.datatype</groupId>
             <artifactId>jackson-datatype-joda</artifactId>
         </dependency>
@@ -104,7 +108,11 @@
         </dependency>
         <dependency>
             <groupId>io.dropwizard.metrics</groupId>
-            <artifactId>metrics-ehcache</artifactId>
+            <artifactId>metrics-jcache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.cache</groupId>
+            <artifactId>cache-api</artifactId>
         </dependency>
         <dependency>
             <groupId>javax.inject</groupId>
@@ -126,10 +134,6 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>net.sf.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.antlr</groupId>
             <artifactId>stringtemplate</artifactId>
         </dependency>
@@ -143,11 +147,20 @@
         </dependency>
         <dependency>
             <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-ehcache</artifactId>
+            <artifactId>shiro-guice</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.apache.shiro</groupId>
-            <artifactId>shiro-guice</artifactId>
+            <groupId>org.ehcache</groupId>
+            <artifactId>ehcache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.ehcache</groupId>
+            <artifactId>ehcache-clustered</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ehcache.integrations.shiro</groupId>
+            <artifactId>shiro-ehcache3</artifactId>
         </dependency>
         <dependency>
             <groupId>org.flywaydb</groupId>
diff --git a/util/src/main/java/org/killbill/billing/util/account/AccountDateTimeUtils.java b/util/src/main/java/org/killbill/billing/util/account/AccountDateTimeUtils.java
index 6ab4322..58f9157 100644
--- a/util/src/main/java/org/killbill/billing/util/account/AccountDateTimeUtils.java
+++ b/util/src/main/java/org/killbill/billing/util/account/AccountDateTimeUtils.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -20,11 +20,20 @@ package org.killbill.billing.util.account;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.TimeZoneAwareEntity;
 
 public abstract class AccountDateTimeUtils {
 
+    public static DateTimeZone getFixedOffsetTimeZone(final TimeZoneAwareEntity account) {
+        return getFixedOffsetTimeZone(account.getTimeZone(), account);
+    }
+
     public static DateTimeZone getFixedOffsetTimeZone(final Account account) {
-        final DateTimeZone referenceDateTimeZone = account.getTimeZone();
+        return getFixedOffsetTimeZone(account.getTimeZone(), account);
+    }
+
+    public static DateTimeZone getFixedOffsetTimeZone(final DateTimeZone referenceDateTimeZone, final Entity account) {
         final DateTime referenceDateTime = getReferenceDateTime(account);
 
         // Check if DST was in effect at the reference date time
@@ -36,7 +45,7 @@ public abstract class AccountDateTimeUtils {
         }
     }
 
-    public static DateTime getReferenceDateTime(final Account account) {
+    public static DateTime getReferenceDateTime(final Entity account) {
         return account.getCreatedDate();
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java
index 7ce819b..0c67fb0 100644
--- a/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java
+++ b/util/src/main/java/org/killbill/billing/util/audit/dao/AuditLogModelDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,26 +18,98 @@
 
 package org.killbill.billing.util.audit.dao;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
 import org.killbill.billing.util.audit.AuditLog;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.dao.EntityAudit;
 import org.killbill.billing.util.dao.TableName;
 import org.killbill.billing.util.entity.dao.EntityModelDao;
 
-public class AuditLogModelDao extends EntityAudit implements EntityModelDao<AuditLog> {
+public class AuditLogModelDao implements EntityModelDao<AuditLog>, Externalizable {
+
+    private UUID id;
+    private DateTime createdDate;
+    private DateTime updatedDate;
+    private TableName tableName;
+    private Long targetRecordId;
+    private ChangeType changeType;
+    private CallContext callContext;
 
-    private final CallContext callContext;
+    private Long recordId;
+    private Long accountRecordId;
+    private Long tenantRecordId;
+
+    // For deserialization
+    public AuditLogModelDao() {}
 
     public AuditLogModelDao(final EntityAudit entityAudit, final CallContext callContext) {
-        super(entityAudit.getId(), entityAudit.getTableName(), entityAudit.getTargetRecordId(), entityAudit.getChangeType(), entityAudit.getCreatedDate());
+        this.id = entityAudit.getId();
+        this.tableName = entityAudit.getTableName();
+        this.targetRecordId = entityAudit.getTargetRecordId();
+        this.changeType = entityAudit.getChangeType();
+        this.createdDate = entityAudit.getCreatedDate();
+        this.updatedDate = null;
         this.callContext = callContext;
     }
 
+    @Override
+    public UUID getId() {
+        return id;
+    }
+
+    @Override
+    public DateTime getCreatedDate() {
+        return createdDate;
+    }
+
+    @Override
+    public DateTime getUpdatedDate() {
+        return updatedDate;
+    }
+
+    @Override
+    public TableName getTableName() {
+        return tableName;
+    }
+
+    public Long getTargetRecordId() {
+        return targetRecordId;
+    }
+
+    public ChangeType getChangeType() {
+        return changeType;
+    }
+
     public CallContext getCallContext() {
         return callContext;
     }
 
     @Override
+    public Long getRecordId() {
+        return recordId;
+    }
+
+    @Override
+    public Long getAccountRecordId() {
+        return accountRecordId;
+    }
+
+    @Override
+    public Long getTenantRecordId() {
+        return tenantRecordId;
+    }
+
+    @Override
     public TableName getHistoryTableName() {
         return null;
     }
@@ -43,7 +117,16 @@ public class AuditLogModelDao extends EntityAudit implements EntityModelDao<Audi
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder("AuditLogModelDao{");
-        sb.append("callContext=").append(callContext);
+        sb.append("id=").append(id);
+        sb.append(", createdDate=").append(createdDate);
+        sb.append(", updatedDate=").append(updatedDate);
+        sb.append(", tableName=").append(tableName);
+        sb.append(", targetRecordId=").append(targetRecordId);
+        sb.append(", changeType=").append(changeType);
+        sb.append(", callContext=").append(callContext);
+        sb.append(", recordId=").append(recordId);
+        sb.append(", accountRecordId=").append(accountRecordId);
+        sb.append(", tenantRecordId=").append(tenantRecordId);
         sb.append('}');
         return sb.toString();
     }
@@ -56,23 +139,61 @@ public class AuditLogModelDao extends EntityAudit implements EntityModelDao<Audi
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
-        if (!super.equals(o)) {
-            return false;
-        }
 
         final AuditLogModelDao that = (AuditLogModelDao) o;
 
+        if (id != null ? !id.equals(that.id) : that.id != null) {
+            return false;
+        }
+        if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
+            return false;
+        }
+        if (updatedDate != null ? updatedDate.compareTo(that.updatedDate) != 0 : that.updatedDate != null) {
+            return false;
+        }
+        if (tableName != that.tableName) {
+            return false;
+        }
+        if (targetRecordId != null ? !targetRecordId.equals(that.targetRecordId) : that.targetRecordId != null) {
+            return false;
+        }
+        if (changeType != that.changeType) {
+            return false;
+        }
         if (callContext != null ? !callContext.equals(that.callContext) : that.callContext != null) {
             return false;
         }
-
-        return true;
+        if (recordId != null ? !recordId.equals(that.recordId) : that.recordId != null) {
+            return false;
+        }
+        if (accountRecordId != null ? !accountRecordId.equals(that.accountRecordId) : that.accountRecordId != null) {
+            return false;
+        }
+        return tenantRecordId != null ? tenantRecordId.equals(that.tenantRecordId) : that.tenantRecordId == null;
     }
 
     @Override
     public int hashCode() {
-        int result = super.hashCode();
+        int result = id != null ? id.hashCode() : 0;
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        result = 31 * result + (updatedDate != null ? updatedDate.hashCode() : 0);
+        result = 31 * result + (tableName != null ? tableName.hashCode() : 0);
+        result = 31 * result + (targetRecordId != null ? targetRecordId.hashCode() : 0);
+        result = 31 * result + (changeType != null ? changeType.hashCode() : 0);
         result = 31 * result + (callContext != null ? callContext.hashCode() : 0);
+        result = 31 * result + (recordId != null ? recordId.hashCode() : 0);
+        result = 31 * result + (accountRecordId != null ? accountRecordId.hashCode() : 0);
+        result = 31 * result + (tenantRecordId != null ? tenantRecordId.hashCode() : 0);
         return result;
     }
+
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java
index d88ea69..6e9955d 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/AccountBCDCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -22,7 +22,7 @@ import java.util.UUID;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
-public class AccountBCDCacheLoader extends BaseCacheLoader {
+public class AccountBCDCacheLoader extends BaseCacheLoader<UUID, Integer> {
 
     @Override
     public CacheType getCacheType() {
@@ -30,30 +30,18 @@ public class AccountBCDCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof UUID)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
-
+    public Integer compute(final UUID key, final CacheLoaderArgument cacheLoaderArgument) {
         if (cacheLoaderArgument.getArgs() == null ||
             !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) {
             throw new IllegalArgumentException("Missing LoaderCallback from the arguments ");
         }
 
         final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
-        return callback.loadAccountBCD((UUID) key, cacheLoaderArgument.getInternalTenantContext());
+        return callback.loadAccountBCD(key, cacheLoaderArgument.getInternalTenantContext());
     }
 
     public interface LoaderCallback {
-        Object loadAccountBCD(final UUID accountId, final InternalTenantContext context);
+
+        Integer loadAccountBCD(final UUID accountId, final InternalTenantContext context);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java
index 352a2d7..7cb8757 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/AccountRecordIdCacheLoader.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -28,10 +28,8 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.skife.jdbi.v2.Handle;
 
-import net.sf.ehcache.loader.CacheLoader;
-
 @Singleton
-public class AccountRecordIdCacheLoader extends BaseIdCacheLoader implements CacheLoader {
+public class AccountRecordIdCacheLoader extends BaseIdCacheLoader<Long> {
 
     private final NonEntityDao nonEntityDao;
 
@@ -47,7 +45,7 @@ public class AccountRecordIdCacheLoader extends BaseIdCacheLoader implements Cac
     }
 
     @Override
-    protected Object doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
+    protected Long doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
         return nonEntityDao.retrieveAccountRecordIdFromObjectInTransaction(UUID.fromString(rawKey), objectType, null, handle);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java
index bc0761d..fac8514 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/AuditLogCacheLoader.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,25 +18,24 @@
 
 package org.killbill.billing.util.cache;
 
+import java.util.List;
+
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
-import org.skife.jdbi.v2.IDBI;
-
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.AuditSqlDao;
-import org.killbill.billing.util.dao.NonEntityDao;
-
-import net.sf.ehcache.loader.CacheLoader;
+import org.skife.jdbi.v2.IDBI;
 
 @Singleton
-public class AuditLogCacheLoader extends BaseCacheLoader implements CacheLoader {
+public class AuditLogCacheLoader extends BaseCacheLoader<String, List<AuditLogModelDao>> {
 
     private final AuditSqlDao auditSqlDao;
 
     @Inject
-    public AuditLogCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+    public AuditLogCacheLoader(final IDBI dbi) {
         super();
         this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
     }
@@ -45,17 +46,8 @@ public class AuditLogCacheLoader extends BaseCacheLoader implements CacheLoader 
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-
-        final Object[] args = ((CacheLoaderArgument) argument).getArgs();
+    public List<AuditLogModelDao> compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
+        final Object[] args = cacheLoaderArgument.getArgs();
         final String tableName = (String) args[0];
         final Long targetRecordId = (Long) args[1];
         final InternalTenantContext internalTenantContext = (InternalTenantContext) args[2];
diff --git a/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java
index b819148..26d215a 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/AuditLogViaHistoryCacheLoader.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,25 +18,24 @@
 
 package org.killbill.billing.util.cache;
 
+import java.util.List;
+
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
-import org.skife.jdbi.v2.IDBI;
-
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.audit.dao.AuditLogModelDao;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.AuditSqlDao;
-import org.killbill.billing.util.dao.NonEntityDao;
-
-import net.sf.ehcache.loader.CacheLoader;
+import org.skife.jdbi.v2.IDBI;
 
 @Singleton
-public class AuditLogViaHistoryCacheLoader extends BaseCacheLoader implements CacheLoader {
+public class AuditLogViaHistoryCacheLoader extends BaseCacheLoader<String, List<AuditLogModelDao>> {
 
     private final AuditSqlDao auditSqlDao;
 
     @Inject
-    public AuditLogViaHistoryCacheLoader(final IDBI dbi, final NonEntityDao nonEntityDao) {
+    public AuditLogViaHistoryCacheLoader(final IDBI dbi) {
         super();
         this.auditSqlDao = dbi.onDemand(AuditSqlDao.class);
     }
@@ -45,17 +46,8 @@ public class AuditLogViaHistoryCacheLoader extends BaseCacheLoader implements Ca
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-
-        final Object[] args = ((CacheLoaderArgument) argument).getArgs();
+    public List<AuditLogModelDao> compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
+        final Object[] args = cacheLoaderArgument.getArgs();
         final String tableName = (String) args[0];
         final String historyTableName = (String) args[1];
         final Long targetRecordId = (Long) args[2];
diff --git a/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java
index c0044c6..973f9f0 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/BaseCacheLoader.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,77 +18,13 @@
 
 package org.killbill.billing.util.cache;
 
-import java.util.Collection;
-import java.util.Map;
-
-import javax.inject.Inject;
-
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Status;
-import net.sf.ehcache.loader.CacheLoader;
-
-public abstract class BaseCacheLoader implements CacheLoader {
+public abstract class BaseCacheLoader<K, V> {
 
     static final String EMPTY_VALUE_PLACEHOLDER = "__#VALEUR!__";
 
-    private Status cacheLoaderStatus;
-
-    @Inject
-    public BaseCacheLoader() {
-        this.cacheLoaderStatus = Status.STATUS_UNINITIALISED;
-    }
-
     public abstract CacheType getCacheType();
 
-    @Override
-    public abstract Object load(final Object key, final Object argument);
-
-    @Override
-    public Object load(final Object key) throws CacheException {
-        throw new IllegalStateException("Method load is not implemented ");
-    }
-
-    @Override
-    public Map loadAll(final Collection keys) {
-        throw new IllegalStateException("Method loadAll is not implemented ");
-    }
-
-    @Override
-    public Map loadAll(final Collection keys, final Object argument) {
-        throw new IllegalStateException("Method loadAll is not implemented ");
-    }
-
-    @Override
-    public String getName() {
-        return this.getClass().getName();
-    }
-
-    @Override
-    public CacheLoader clone(final Ehcache cache) throws CloneNotSupportedException {
-        throw new IllegalStateException("Method clone is not implemented ");
-    }
-
-    @Override
-    public void init() {
-        this.cacheLoaderStatus = Status.STATUS_ALIVE;
-    }
-
-    @Override
-    public void dispose() throws CacheException {
-        cacheLoaderStatus = Status.STATUS_SHUTDOWN;
-    }
-
-    @Override
-    public Status getStatus() {
-        return cacheLoaderStatus;
-    }
-
-    protected void checkCacheLoaderStatus() {
-        if (getStatus() != Status.STATUS_ALIVE) {
-            throw new CacheException("CacheLoader is not available!");
-        }
-    }
+    public abstract V compute(final K key, final CacheLoaderArgument cacheLoaderArgument);
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/BaseIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/BaseIdCacheLoader.java
index c0329ae..e2cb993 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/BaseIdCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/BaseIdCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,40 +18,28 @@
 package org.killbill.billing.util.cache;
 
 import org.killbill.billing.ObjectType;
-import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.skife.jdbi.v2.Handle;
 
-public abstract class BaseIdCacheLoader extends BaseCacheLoader {
+public abstract class BaseIdCacheLoader<V> extends BaseCacheLoader<String, V> {
 
     protected BaseIdCacheLoader() {
         super();
     }
 
-    @Override
-    public abstract CacheType getCacheType();
-
-    protected abstract Object doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle);
+    protected abstract V doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle);
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-
+    public V compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
         final String rawKey;
         if (getCacheType().isKeyPrefixedWithTableName()) {
-            final String[] parts = ((String) key).split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+            final String[] parts = key.split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
             rawKey = parts[1];
         } else {
-            rawKey = (String) key;
+            rawKey = key;
         }
-        final ObjectType objectType = ((CacheLoaderArgument) argument).getObjectType();
-        final Handle handle = ((CacheLoaderArgument) argument).getHandle();
+
+        final ObjectType objectType = cacheLoaderArgument.getObjectType();
+        final Handle handle = cacheLoaderArgument.getHandle();
         return doRetrieveOperation(rawKey, objectType, handle);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
index a70556c..97d5cb6 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -20,6 +22,14 @@ import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.catalog.api.Catalog;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.util.config.tenant.PerTenantConfig;
 
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.METHOD})
@@ -43,58 +53,63 @@ public @interface Cachable {
 
     CacheType value();
 
+    // Make sure both the key and value are Serializable
     enum CacheType {
 
-        /* Mapping from object 'id (UUID)' -> object 'recordId (Long' */
-        RECORD_ID(RECORD_ID_CACHE_NAME, false),
+        /* Mapping from object 'id (UUID as String)' -> object 'recordId (Long)' */
+        RECORD_ID(RECORD_ID_CACHE_NAME, String.class, Long.class, false),
 
-        /* Mapping from object 'id (UUID)' -> matching account object 'accountRecordId (Long)' */
-        ACCOUNT_RECORD_ID(ACCOUNT_RECORD_ID_CACHE_NAME, false),
+        /* Mapping from object 'id (UUID as String)' -> matching account object 'accountRecordId (Long)' */
+        ACCOUNT_RECORD_ID(ACCOUNT_RECORD_ID_CACHE_NAME, String.class, Long.class, false),
 
-        /* Mapping from object 'id (UUID)' -> matching object 'tenantRecordId (Long)' */
-        TENANT_RECORD_ID(TENANT_RECORD_ID_CACHE_NAME, false),
+        /* Mapping from object 'id (UUID as String)' -> matching object 'tenantRecordId (Long)' */
+        TENANT_RECORD_ID(TENANT_RECORD_ID_CACHE_NAME, String.class, Long.class, false),
 
-        /* Mapping from object 'recordId (Long') -> object 'id (UUID)'  */
-        OBJECT_ID(OBJECT_ID_CACHE_NAME, true),
+        /* Mapping from object 'recordId (Long as String)' -> object 'id (UUID)'  */
+        OBJECT_ID(OBJECT_ID_CACHE_NAME, String.class, UUID.class, true),
 
-        /* Mapping from object 'tableName::targetRecordId' -> matching objects 'Iterable<AuditLog>' */
-        AUDIT_LOG(AUDIT_LOG_CACHE_NAME, true),
+        /* Mapping from object 'tableName::targetRecordId' -> matching objects 'List<AuditLogModelDao>' */
+        AUDIT_LOG(AUDIT_LOG_CACHE_NAME, String.class, List.class, true),
 
-        /* Mapping from object 'tableName::historyTableName::targetRecordId' -> matching objects 'Iterable<AuditLog>' */
-        AUDIT_LOG_VIA_HISTORY(AUDIT_LOG_VIA_HISTORY_CACHE_NAME, true),
+        /* Mapping from object 'tableName::historyTableName::targetRecordId' -> matching objects 'List<AuditLogModelDao>' */
+        AUDIT_LOG_VIA_HISTORY(AUDIT_LOG_VIA_HISTORY_CACHE_NAME, String.class, List.class, true),
 
         /* Tenant catalog cache */
-        TENANT_CATALOG(TENANT_CATALOG_CACHE_NAME, false),
+        TENANT_CATALOG(TENANT_CATALOG_CACHE_NAME, Long.class, Catalog.class, false),
 
-        /* Tenant payment state machine config cache */
-        TENANT_PAYMENT_STATE_MACHINE_CONFIG(TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME, false),
+        /* Tenant payment state machine config cache (String -> SerializableStateMachineConfig) */
+        TENANT_PAYMENT_STATE_MACHINE_CONFIG(TENANT_PAYMENT_STATE_MACHINE_CONFIG_CACHE_NAME, String.class, Object.class, false),
 
-        /* Tenant overdue config cache */
-        TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, false),
+        /* Tenant overdue config cache (String -> DefaultOverdueConfig) */
+        TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, Long.class, Object.class, false),
 
         /* Tenant overdue config cache */
-        TENANT_CONFIG(TENANT_CONFIG_CACHE_NAME, false),
+        TENANT_CONFIG(TENANT_CONFIG_CACHE_NAME, Long.class, PerTenantConfig.class, false),
 
         /* Tenant config cache */
-        TENANT_KV(TENANT_KV_CACHE_NAME, false),
+        TENANT_KV(TENANT_KV_CACHE_NAME, String.class, String.class, false),
 
-        /* Tenant config cache */
-        TENANT(TENANT_CACHE_NAME, false),
+        /* Tenant cache */
+        TENANT(TENANT_CACHE_NAME, String.class, Tenant.class, false),
 
         /* Overwritten plans  */
-        OVERRIDDEN_PLAN(OVERRIDDEN_PLAN_CACHE_NAME, false),
+        OVERRIDDEN_PLAN(OVERRIDDEN_PLAN_CACHE_NAME, String.class, Plan.class, false),
 
         /* Immutable account data config cache */
-        ACCOUNT_IMMUTABLE(ACCOUNT_IMMUTABLE_CACHE_NAME, false),
+        ACCOUNT_IMMUTABLE(ACCOUNT_IMMUTABLE_CACHE_NAME, Long.class, ImmutableAccountData.class, false),
 
         /* Account BCD config cache */
-        ACCOUNT_BCD(ACCOUNT_BCD_CACHE_NAME, false);
+        ACCOUNT_BCD(ACCOUNT_BCD_CACHE_NAME, UUID.class, Integer.class, false);
 
         private final String cacheName;
+        private final Class keyType;
+        private final Class valueType;
         private final boolean isKeyPrefixedWithTableName;
 
-        CacheType(final String cacheName, final boolean isKeyPrefixedWithTableName) {
+        CacheType(final String cacheName, final Class keyType, final Class valueType, final boolean isKeyPrefixedWithTableName) {
             this.cacheName = cacheName;
+            this.keyType = keyType;
+            this.valueType = valueType;
             this.isKeyPrefixedWithTableName = isKeyPrefixedWithTableName;
         }
 
@@ -102,6 +117,14 @@ public @interface Cachable {
             return cacheName;
         }
 
+        public Class<?> getKeyType() {
+            return keyType;
+        }
+
+        public Class<?> getValueType() {
+            return valueType;
+        }
+
         public boolean isKeyPrefixedWithTableName() { return isKeyPrefixedWithTableName; }
 
         public static CacheType findByName(final String input) {
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheController.java b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java
index 2c6be1e..46ee5dc 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheController.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheController.java
@@ -16,18 +16,24 @@
 
 package org.killbill.billing.util.cache;
 
+import java.util.List;
+
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
+import com.google.common.base.Function;
+
 public interface CacheController<K, V> {
 
-    void add(K key, V value);
+    List<K> getKeys();
 
-    V get(K key, CacheLoaderArgument objectType);
+    boolean isKeyInCache(K key);
 
-    V get(K key);
+    V get(K key, CacheLoaderArgument objectType);
 
     boolean remove(K key);
 
+    void remove(Function<K, Boolean> keyMatcher);
+
     void putIfAbsent(final K key, V value);
 
     int size();
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
index bdf1302..2d9b966 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcher.java
@@ -44,8 +44,8 @@ public class CacheControllerDispatcher {
         caches = new HashMap<CacheType, CacheController<Object, Object>>();
     }
 
-    public CacheController<Object, Object> getCacheController(final CacheType cacheType) {
-        return caches.get(cacheType);
+    public <K, V> CacheController<K, V> getCacheController(final CacheType cacheType) {
+        return cast(caches.get(cacheType));
     }
 
     public void clearAll() {
@@ -53,4 +53,9 @@ public class CacheControllerDispatcher {
             cacheController.removeAll();
         }
     }
+
+    @SuppressWarnings("unchecked")
+    private static <K, V> CacheController<K, V> cast(final CacheController<?, ?> cache) {
+        return (CacheController<K, V>) cache;
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java
index f980ad7..7d9ce76 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheControllerDispatcherProvider.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,10 +18,12 @@
 
 package org.killbill.billing.util.cache;
 
-import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
+import javax.cache.Cache;
+import javax.cache.CacheManager;
 import javax.inject.Inject;
 import javax.inject.Provider;
 
@@ -29,53 +31,37 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.loader.CacheLoader;
-
 // Build the abstraction layer between EhCache and Kill Bill
 public class CacheControllerDispatcherProvider implements Provider<CacheControllerDispatcher> {
 
     private static final Logger logger = LoggerFactory.getLogger(CacheControllerDispatcherProvider.class);
 
     private final CacheManager cacheManager;
+    private final Set<BaseCacheLoader> cacheLoaders;
 
     @Inject
-    public CacheControllerDispatcherProvider(final CacheManager cacheManager) {
+    public CacheControllerDispatcherProvider(final CacheManager cacheManager,
+                                             final Set<BaseCacheLoader> cacheLoaders) {
         this.cacheManager = cacheManager;
+        this.cacheLoaders = cacheLoaders;
     }
 
     @Override
     public CacheControllerDispatcher get() {
         final Map<CacheType, CacheController<Object, Object>> cacheControllers = new LinkedHashMap<CacheType, CacheController<Object, Object>>();
-        for (final String cacheName : cacheManager.getCacheNames()) {
-            final CacheType cacheType = CacheType.findByName(cacheName);
+        for (final BaseCacheLoader cacheLoader : cacheLoaders) {
+            final CacheType cacheType = cacheLoader.getCacheType();
 
-            final Collection<EhCacheBasedCacheController<Object, Object>> cacheControllersForCacheName = getCacheControllersForCacheName(cacheName, cacheType);
-            // EhCache supports multiple cache loaders per type, but not Kill Bill - take the first one
-            if (cacheControllersForCacheName.size() > 0) {
-                final EhCacheBasedCacheController<Object, Object> ehCacheBasedCacheController = cacheControllersForCacheName.iterator().next();
-                cacheControllers.put(cacheType, ehCacheBasedCacheController);
+            final Cache cache = cacheManager.getCache(cacheType.getCacheName(), cacheType.getKeyType(), cacheType.getValueType());
+            if (cache == null) {
+                logger.warn("Cache for cacheName='{}' not configured - check your ehcache.xml", cacheLoader.getCacheType().getCacheName());
+                continue;
             }
-        }
-        return new CacheControllerDispatcher(cacheControllers);
-    }
 
-    private Collection<EhCacheBasedCacheController<Object, Object>> getCacheControllersForCacheName(final String name, final CacheType cacheType) {
-        final Ehcache cache = cacheManager.getEhcache(name);
-        if (cache == null) {
-            logger.warn("No cache configured for name {}", name);
-            return ImmutableList.<EhCacheBasedCacheController<Object, Object>>of();
+            final CacheController<Object, Object> ehCacheBasedCacheController = new EhCacheBasedCacheController<Object, Object>(cache, cacheLoader);
+            cacheControllers.put(cacheType, ehCacheBasedCacheController);
         }
-        // The CacheLoaders were registered in EhCacheCacheManagerProvider
-        return Collections2.transform(cache.getRegisteredCacheLoaders(), new Function<CacheLoader, EhCacheBasedCacheController<Object, Object>>() {
-            @Override
-            public EhCacheBasedCacheController<Object, Object> apply(final CacheLoader input) {
-                return new EhCacheBasedCacheController<Object, Object>(cache, cacheType);
-            }
-        });
+
+        return new CacheControllerDispatcher(cacheControllers);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java b/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java
index f559c0f..1724c59 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/CacheLoaderArgument.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,6 +18,8 @@
 
 package org.killbill.billing.util.cache;
 
+import java.util.Arrays;
+
 import javax.annotation.Nullable;
 
 import org.killbill.billing.ObjectType;
@@ -61,4 +63,14 @@ public class CacheLoaderArgument {
     public Handle getHandle() {
         return handle;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("CacheLoaderArgument{");
+        sb.append("objectType=").append(objectType);
+        sb.append(", args=").append(Arrays.toString(args));
+        sb.append(", internalTenantContext=").append(internalTenantContext);
+        sb.append('}');
+        return sb.toString();
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
index 08d04d2..c53b879 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2012 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -18,69 +18,127 @@
 
 package org.killbill.billing.util.cache;
 
-import javax.annotation.Nullable;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.cache.Cache;
+import javax.cache.Cache.Entry;
 
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 
 public class EhCacheBasedCacheController<K, V> implements CacheController<K, V> {
 
-    private final Ehcache cache;
-    private final CacheType cacheType;
+    private static final Logger logger = LoggerFactory.getLogger(EhCacheBasedCacheController.class);
+
+    private final Cache<K, V> cache;
+    private final BaseCacheLoader<K, V> baseCacheLoader;
 
-    public EhCacheBasedCacheController(final Ehcache cache, final CacheType cacheType) {
+    public EhCacheBasedCacheController(final Cache<K, V> cache, final BaseCacheLoader<K, V> baseCacheLoader) {
         this.cache = cache;
-        this.cacheType = cacheType;
+        this.baseCacheLoader = baseCacheLoader;
     }
 
     @Override
-    public void add(final K key, final V value) {
-        cache.putIfAbsent(new Element(key, value));
+    public List<K> getKeys() {
+        final Iterable<K> kIterable = Iterables.<Entry<K, V>, K>transform(cache,
+                                                                          new Function<Entry<K, V>, K>() {
+                                                                              @Override
+                                                                              public K apply(final Entry<K, V> input) {
+                                                                                  return input.getKey();
+                                                                              }
+                                                                          });
+        return ImmutableList.<K>copyOf(kIterable);
     }
 
     @Override
-    public V get(final K key, @Nullable final CacheLoaderArgument cacheLoaderArgument) {
-        return getWithOrWithoutCacheLoaderArgument(key, cacheLoaderArgument);
+    public boolean isKeyInCache(final K key) {
+        return cache.containsKey(key);
     }
 
     @Override
-    public V get(final K key) {
-        return getWithOrWithoutCacheLoaderArgument(key, null);
+    public V get(final K key, final CacheLoaderArgument cacheLoaderArgument) {
+        if (key == null) {
+            return null;
+        }
+
+        final V value;
+        if (!isKeyInCache(key)) {
+            value = computeAndCacheValue(key, cacheLoaderArgument);
+        } else {
+            value = cache.get(key);
+        }
+
+        if (value == null || value.equals(BaseCacheLoader.EMPTY_VALUE_PLACEHOLDER)) {
+            return null;
+        } else {
+            return value;
+        }
     }
 
-    public void putIfAbsent(final K key, V value) {
-        final Element element = new Element(key, value);
-        cache.putIfAbsent(element);
+    @Override
+    public void putIfAbsent(final K key, final V value) {
+        cache.putIfAbsent(key, value);
     }
 
     @Override
     public boolean remove(final K key) {
-        return cache.remove(key);
+        if (isKeyInCache(key)) {
+            cache.remove(key);
+            return true;
+        } else {
+            return false;
+        }
     }
 
     @Override
-    public int size() {
-        return cache.getSize();
+    public void remove(final Function<K, Boolean> keyMatcher) {
+        final Set<K> toRemove = new HashSet<K>();
+        for (final Object key : getKeys()) {
+            if (keyMatcher.apply((K) key) == Boolean.TRUE) {
+                toRemove.add((K) key);
+            }
+        }
+        cache.removeAll(toRemove);
     }
 
     @Override
     public void removeAll() {
-        cache.removeAll();
+        cache.clear();
+    }
+
+    @Override
+    public int size() {
+        return Iterables.<Cache.Entry<K, V>>size(cache);
     }
 
     @Override
     public CacheType getCacheType() {
-        return cacheType;
+        return baseCacheLoader.getCacheType();
     }
 
-    private V getWithOrWithoutCacheLoaderArgument(final K key, @Nullable final CacheLoaderArgument cacheLoaderArgument) {
-        final Element element = cacheLoaderArgument != null ? cache.getWithLoader(key, null, cacheLoaderArgument) : cache.get(key);
-        if (element == null || element.getObjectValue() == null || element.getObjectValue().equals(BaseCacheLoader.EMPTY_VALUE_PLACEHOLDER)) {
+    private V computeAndCacheValue(final K key, final CacheLoaderArgument cacheLoaderArgument) {
+        final V value;
+        try {
+            value = baseCacheLoader.compute(key, cacheLoaderArgument);
+        } catch (final Exception e) {
+            logger.warn("Unable to compute cached value for key='{}' and cacheLoaderArgument='{}'", key, cacheLoaderArgument, e);
+            throw new RuntimeException(e);
+        }
+
+        if (value == null) {
             return null;
         }
-        return (V) element.getObjectValue();
-    }
 
+        // Race condition, we may compute it for nothing
+        putIfAbsent(key, value);
+
+        return value;
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/ExternalizableInput.java b/util/src/main/java/org/killbill/billing/util/cache/ExternalizableInput.java
new file mode 100644
index 0000000..73e45a7
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/ExternalizableInput.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInput;
+
+// See http://www.cowtowncoder.com/blog/archives/2012/08/entry_477.html
+public class ExternalizableInput extends InputStream {
+
+    private final ObjectInput in;
+
+    public ExternalizableInput(final ObjectInput in) {
+        this.in = in;
+    }
+
+    @Override
+    public int available() throws IOException {
+        return in.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        in.close();
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public int read() throws IOException {
+        return in.read();
+    }
+
+    @Override
+    public int read(final byte[] buffer) throws IOException {
+        return in.read(buffer);
+    }
+
+    @Override
+    public int read(final byte[] buffer, final int offset, final int len) throws IOException {
+        return in.read(buffer, offset, len);
+    }
+
+    @Override
+    public long skip(final long n) throws IOException {
+        return in.skip(n);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/ExternalizableOutput.java b/util/src/main/java/org/killbill/billing/util/cache/ExternalizableOutput.java
new file mode 100644
index 0000000..e5297d4
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/ExternalizableOutput.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.cache;
+
+import java.io.IOException;
+import java.io.ObjectOutput;
+import java.io.OutputStream;
+
+// See http://www.cowtowncoder.com/blog/archives/2012/08/entry_477.html
+public class ExternalizableOutput extends OutputStream {
+
+    private final ObjectOutput out;
+
+    public ExternalizableOutput(final ObjectOutput out) {
+        this.out = out;
+    }
+
+    @Override
+    public void flush() throws IOException {
+        out.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        out.close();
+    }
+
+    @Override
+    public void write(final int ch) throws IOException {
+        out.write(ch);
+    }
+
+    @Override
+    public void write(final byte[] data) throws IOException {
+        out.write(data);
+    }
+
+    @Override
+    public void write(final byte[] data, final int offset, final int len) throws IOException {
+        out.write(data, offset, len);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java
index 866c44e..8634a01 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/ImmutableAccountCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -17,10 +17,11 @@
 
 package org.killbill.billing.util.cache;
 
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
-public class ImmutableAccountCacheLoader extends BaseCacheLoader {
+public class ImmutableAccountCacheLoader extends BaseCacheLoader<Long, ImmutableAccountData> {
 
     @Override
     public CacheType getCacheType() {
@@ -28,30 +29,18 @@ public class ImmutableAccountCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof Long)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
-
+    public ImmutableAccountData compute(final Long key, final CacheLoaderArgument cacheLoaderArgument) {
         if (cacheLoaderArgument.getArgs() == null ||
             !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) {
             throw new IllegalArgumentException("Missing LoaderCallback from the arguments ");
         }
 
         final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
-        return callback.loadAccount((Long) key, cacheLoaderArgument.getInternalTenantContext());
+        return callback.loadAccount(key, cacheLoaderArgument.getInternalTenantContext());
     }
 
     public interface LoaderCallback {
-        Object loadAccount(final Long recordId, final InternalTenantContext context);
+
+        ImmutableAccountData loadAccount(final Long recordId, final InternalTenantContext context);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/MapperHolder.java b/util/src/main/java/org/killbill/billing/util/cache/MapperHolder.java
new file mode 100644
index 0000000..65a8f8a
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/MapperHolder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.cache;
+
+import org.killbill.billing.util.jackson.ObjectMapper;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+
+import static com.fasterxml.jackson.core.JsonGenerator.Feature.AUTO_CLOSE_TARGET;
+
+// See http://www.cowtowncoder.com/blog/archives/2012/08/entry_477.html
+public class MapperHolder {
+
+    private static final MapperHolder instance = new MapperHolder();
+
+    static {
+        instance.mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
+        instance.mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
+        // Stream is NOT owned by Jackson
+        instance.mapper.disable(AUTO_CLOSE_TARGET);
+    }
+
+    private final SmileFactory f = new SmileFactory();
+    private final ObjectMapper mapper = new ObjectMapper(f);
+
+    public static ObjectMapper mapper() { return instance.mapper; }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/cache/ObjectIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/ObjectIdCacheLoader.java
index fd357ab..cda30b6 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/ObjectIdCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/ObjectIdCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -17,6 +17,8 @@
 
 package org.killbill.billing.util.cache;
 
+import java.util.UUID;
+
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
@@ -25,10 +27,8 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.skife.jdbi.v2.Handle;
 
-import net.sf.ehcache.loader.CacheLoader;
-
 @Singleton
-public class ObjectIdCacheLoader extends BaseIdCacheLoader implements CacheLoader {
+public class ObjectIdCacheLoader extends BaseIdCacheLoader<UUID> {
 
     private final NonEntityDao nonEntityDao;
 
@@ -44,7 +44,7 @@ public class ObjectIdCacheLoader extends BaseIdCacheLoader implements CacheLoade
     }
 
     @Override
-    protected Object doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
+    protected UUID doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
         final Long recordId = Long.valueOf(rawKey);
         return nonEntityDao.retrieveIdFromObjectInTransaction(recordId, objectType, null, handle);
     }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/OverriddenPlanCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/OverriddenPlanCacheLoader.java
index 52c4a07..79d73b8 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/OverriddenPlanCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/OverriddenPlanCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -22,13 +22,14 @@ import javax.inject.Singleton;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Plan;
 import org.killbill.billing.catalog.api.StaticCatalog;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class OverriddenPlanCacheLoader extends BaseCacheLoader {
+public class OverriddenPlanCacheLoader extends BaseCacheLoader<String, Plan> {
 
     private final Logger log = LoggerFactory.getLogger(OverriddenPlanCacheLoader.class);
 
@@ -43,18 +44,7 @@ public class OverriddenPlanCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
+    public Plan compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
         if (cacheLoaderArgument.getArgs() == null || cacheLoaderArgument.getArgs().length != 2) {
             throw new IllegalArgumentException("Invalid arguments for overridden plans");
         }
@@ -66,11 +56,10 @@ public class OverriddenPlanCacheLoader extends BaseCacheLoader {
             throw new IllegalArgumentException("Invalid arguments for overridden plans: missing catalog from argument");
         }
 
-
-        final String planName = (String) key;
+        final String planName = key;
         final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
         final StaticCatalog catalog = (StaticCatalog) cacheLoaderArgument.getArgs()[1];
-        final InternalTenantContext internalTenantContext = ((CacheLoaderArgument) argument).getInternalTenantContext();
+        final InternalTenantContext internalTenantContext = cacheLoaderArgument.getInternalTenantContext();
         try {
             log.info("Loading overridden plan {} for tenant {}", planName, internalTenantContext.getTenantRecordId());
 
@@ -82,6 +71,7 @@ public class OverriddenPlanCacheLoader extends BaseCacheLoader {
     }
 
     public interface LoaderCallback {
-        public Object loadPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException;
+
+        public Plan loadPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException;
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java
index 08efe2e..a7027a8 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/RecordIdCacheLoader.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2012 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -28,10 +28,8 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.skife.jdbi.v2.Handle;
 
-import net.sf.ehcache.loader.CacheLoader;
-
 @Singleton
-public class RecordIdCacheLoader extends BaseIdCacheLoader implements CacheLoader {
+public class RecordIdCacheLoader extends BaseIdCacheLoader<Long> {
 
     private final NonEntityDao nonEntityDao;
 
@@ -47,7 +45,7 @@ public class RecordIdCacheLoader extends BaseIdCacheLoader implements CacheLoade
     }
 
     @Override
-    protected Object doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
+    protected Long doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
         return nonEntityDao.retrieveRecordIdFromObjectInTransaction(UUID.fromString(rawKey), objectType, null, handle);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantCacheLoader.java
index bacafa8..1ed03a5 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -26,7 +26,7 @@ import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
 @Singleton
-public class TenantCacheLoader extends BaseCacheLoader {
+public class TenantCacheLoader extends BaseCacheLoader<String, Tenant> {
 
     private final TenantInternalApi tenantApi;
 
@@ -42,19 +42,10 @@ public class TenantCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-
+    public Tenant compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
         try {
-            return tenantApi.getTenantByApiKey((String) key);
-        } catch (TenantApiException e) {
+            return tenantApi.getTenantByApiKey(key);
+        } catch (final TenantApiException e) {
             throw new IllegalStateException("TenantCacheLoader cannot find value for key " + key);
         }
     }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
index b3f9305..03a4ee3 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantCatalogCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -23,6 +23,7 @@ import javax.inject.Inject;
 import javax.inject.Singleton;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.catalog.api.Catalog;
 import org.killbill.billing.catalog.api.CatalogApiException;
 import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
@@ -30,7 +31,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class TenantCatalogCacheLoader extends BaseCacheLoader {
+public class TenantCatalogCacheLoader extends BaseCacheLoader<Long, Catalog> {
 
     private final Logger log = LoggerFactory.getLogger(TenantCatalogCacheLoader.class);
 
@@ -48,19 +49,9 @@ public class TenantCatalogCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof Long)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-        final Long tenantRecordId = (Long) key;
+    public Catalog compute(final Long key, final CacheLoaderArgument cacheLoaderArgument) {
+        final Long tenantRecordId = key;
         final InternalTenantContext internalTenantContext = new InternalTenantContext(tenantRecordId);
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
 
         if (cacheLoaderArgument.getArgs() == null || !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) {
             throw new IllegalArgumentException("Missing LoaderCallback from the arguments ");
@@ -80,6 +71,7 @@ public class TenantCatalogCacheLoader extends BaseCacheLoader {
     }
 
     public interface LoaderCallback {
-        public Object loadCatalog(final List<String> catalogXMLs, final Long tenantRecordId) throws CatalogApiException;
+
+        public Catalog loadCatalog(final List<String> catalogXMLs, final Long tenantRecordId) throws CatalogApiException;
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantConfigCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantConfigCacheLoader.java
index caced95..d71ad0d 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantConfigCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantConfigCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -25,11 +25,12 @@ import javax.inject.Singleton;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.config.tenant.PerTenantConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class TenantConfigCacheLoader extends BaseCacheLoader {
+public class TenantConfigCacheLoader extends BaseCacheLoader<Long, PerTenantConfig> {
 
     private final Logger log = LoggerFactory.getLogger(TenantConfigCacheLoader.class);
 
@@ -47,19 +48,9 @@ public class TenantConfigCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof Long)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-        final Long tenantRecordId = (Long) key;
+    public PerTenantConfig compute(final Long key, final CacheLoaderArgument cacheLoaderArgument) {
+        final Long tenantRecordId = key;
         final InternalTenantContext internalTenantContext = new InternalTenantContext(tenantRecordId);
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
 
         if (cacheLoaderArgument.getArgs() == null || !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) {
             throw new IllegalArgumentException("Missing LoaderCallback from the arguments ");
@@ -77,7 +68,7 @@ public class TenantConfigCacheLoader extends BaseCacheLoader {
     }
 
     public interface LoaderCallback {
-        public Object loadConfig(final String inputJson) throws IOException;
-    }
 
+        public PerTenantConfig loadConfig(final String inputJson) throws IOException;
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantKVCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantKVCacheLoader.java
index a949445..5dcbca5 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantKVCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantKVCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -27,7 +27,7 @@ import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 
 @Singleton
-public class TenantKVCacheLoader extends BaseCacheLoader {
+public class TenantKVCacheLoader extends BaseCacheLoader<String, String> {
 
     private final TenantInternalApi tenantApi;
 
@@ -43,16 +43,8 @@ public class TenantKVCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-        final String[] parts = ((String) key).split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+    public String compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
+        final String[] parts = key.split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
         final String rawKey = parts[0];
         final String tenantRecordId = parts[1];
 
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantOverdueConfigCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantOverdueConfigCacheLoader.java
index 81ebe00..6c95814 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantOverdueConfigCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantOverdueConfigCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -22,13 +22,14 @@ import javax.inject.Singleton;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.overdue.api.OverdueApiException;
+import org.killbill.billing.overdue.api.OverdueConfig;
 import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class TenantOverdueConfigCacheLoader extends BaseCacheLoader {
+public class TenantOverdueConfigCacheLoader extends BaseCacheLoader<Long, Object> {
 
     private static final Logger log = LoggerFactory.getLogger(TenantOverdueConfigCacheLoader.class);
 
@@ -46,19 +47,9 @@ public class TenantOverdueConfigCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof Long)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected argument type of " + argument.getClass().getName());
-        }
-
-        final Long tenantRecordId = (Long) key;
+    public Object compute(final Long key, final CacheLoaderArgument cacheLoaderArgument) {
+        final Long tenantRecordId = key;
         final InternalTenantContext internalTenantContext = new InternalTenantContext(tenantRecordId);
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
 
         if (cacheLoaderArgument.getArgs() == null || !(cacheLoaderArgument.getArgs()[0] instanceof LoaderCallback)) {
             throw new IllegalArgumentException("Missing LoaderCallback from the arguments");
@@ -81,6 +72,6 @@ public class TenantOverdueConfigCacheLoader extends BaseCacheLoader {
 
     public interface LoaderCallback {
 
-        public Object loadOverdueConfig(final String overdueConfigXML) throws OverdueApiException;
+        public OverdueConfig loadOverdueConfig(final String overdueConfigXML) throws OverdueApiException;
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java
index 17a62af..1144e59 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantRecordIdCacheLoader.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -28,10 +28,8 @@ import org.killbill.billing.util.cache.Cachable.CacheType;
 import org.killbill.billing.util.dao.NonEntityDao;
 import org.skife.jdbi.v2.Handle;
 
-import net.sf.ehcache.loader.CacheLoader;
-
 @Singleton
-public class TenantRecordIdCacheLoader extends BaseIdCacheLoader implements CacheLoader {
+public class TenantRecordIdCacheLoader extends BaseIdCacheLoader<Long> {
 
     private final NonEntityDao nonEntityDao;
 
@@ -47,7 +45,7 @@ public class TenantRecordIdCacheLoader extends BaseIdCacheLoader implements Cach
     }
 
     @Override
-    protected Object doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
+    protected Long doRetrieveOperation(final String rawKey, final ObjectType objectType, final Handle handle) {
         return nonEntityDao.retrieveTenantRecordIdFromObjectInTransaction(UUID.fromString(rawKey), objectType, null, handle);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
index c6d5489..6ef8e00 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantStateMachineConfigCacheLoader.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Groupon, Inc
- * Copyright 2016 The Billing Project, LLC
+ * Copyright 2016-2017 Groupon, Inc
+ * Copyright 2016-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -32,7 +32,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader {
+public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader<String, Object> {
 
     private static final Pattern PATTERN = Pattern.compile(TenantKey.PLUGIN_PAYMENT_STATE_MACHINE_.toString() + "(.*)");
     private static final Logger log = LoggerFactory.getLogger(TenantStateMachineConfigCacheLoader.class);
@@ -51,17 +51,8 @@ public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader {
     }
 
     @Override
-    public Object load(final Object key, final Object argument) {
-        checkCacheLoaderStatus();
-
-        if (!(key instanceof String)) {
-            throw new IllegalArgumentException("Unexpected key type of " + key.getClass().getName());
-        }
-        if (!(argument instanceof CacheLoaderArgument)) {
-            throw new IllegalArgumentException("Unexpected key type of " + argument.getClass().getName());
-        }
-
-        final String[] parts = ((String) key).split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
+    public Object compute(final String key, final CacheLoaderArgument cacheLoaderArgument) {
+        final String[] parts = key.split(CacheControllerDispatcher.CACHE_KEY_SEPARATOR);
         final String rawKey = parts[0];
         final Matcher matcher = PATTERN.matcher(rawKey);
         if (!matcher.matches()) {
@@ -70,7 +61,6 @@ public class TenantStateMachineConfigCacheLoader extends BaseCacheLoader {
         final String pluginName = matcher.group(1);
         final String tenantRecordId = parts[1];
 
-        final CacheLoaderArgument cacheLoaderArgument = (CacheLoaderArgument) argument;
         final LoaderCallback callback = (LoaderCallback) cacheLoaderArgument.getArgs()[0];
 
         final InternalTenantContext internalTenantContext = new InternalTenantContext(Long.valueOf(tenantRecordId));
diff --git a/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
index f85796b..a42fe24 100644
--- a/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
+++ b/util/src/main/java/org/killbill/billing/util/callcontext/InternalCallContextFactory.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2012 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -31,18 +31,23 @@ import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.account.AccountDateTimeUtils;
 import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CacheController;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.entity.dao.TimeZoneAwareEntity;
 import org.killbill.clock.Clock;
 import org.slf4j.MDC;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 
 // Internal contexts almost always expect accountRecordId and tenantRecordId to be populated
 public class InternalCallContextFactory {
 
-    public static final long INTERNAL_TENANT_RECORD_ID = 0L;
+    // Long, not long, to avoid NPE with ==
+    public static final Long INTERNAL_TENANT_RECORD_ID = 0L;
 
     public static final String MDC_KB_ACCOUNT_RECORD_ID = "kb.accountRecordId";
     public static final String MDC_KB_TENANT_RECORD_ID = "kb.tenantRecordId";
@@ -50,7 +55,10 @@ public class InternalCallContextFactory {
     private final ImmutableAccountInternalApi accountInternalApi;
     private final Clock clock;
     private final NonEntityDao nonEntityDao;
-    private final CacheControllerDispatcher cacheControllerDispatcher;
+    private final CacheController<String, UUID> objectIdCacheController;
+    private final CacheController<String, Long> recordIdCacheController;
+    private final CacheController<String, Long> accountRecordIdCacheController;
+    private final CacheController<String, Long> tenantRecordIdCacheController;
 
     @Inject
     public InternalCallContextFactory(@Nullable final ImmutableAccountInternalApi accountInternalApi,
@@ -60,7 +68,17 @@ public class InternalCallContextFactory {
         this.accountInternalApi = accountInternalApi;
         this.clock = clock;
         this.nonEntityDao = nonEntityDao;
-        this.cacheControllerDispatcher = cacheControllerDispatcher;
+        if (cacheControllerDispatcher == null) {
+            this.objectIdCacheController = null;
+            this.recordIdCacheController = null;
+            this.accountRecordIdCacheController = null;
+            this.tenantRecordIdCacheController = null;
+        } else {
+            this.objectIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
+            this.recordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
+            this.accountRecordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID);
+            this.tenantRecordIdCacheController = cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID);
+        }
     }
 
     //
@@ -138,7 +156,10 @@ public class InternalCallContextFactory {
         if (accountRecordId == null) {
             return new InternalTenantContext(tenantRecordId);
         } else {
-            return new InternalTenantContext(tenantRecordId, accountRecordId, getFixedOffsetTimeZone(accountRecordId, tenantRecordId), getReferenceTime(accountRecordId, tenantRecordId));
+            final ImmutableAccountData immutableAccountData = getImmutableAccountData(accountRecordId, tenantRecordId);
+            final DateTimeZone fixedOffsetTimeZone = immutableAccountData.getFixedOffsetTimeZone();
+            final DateTime referenceTime = immutableAccountData.getReferenceTime();
+            return new InternalTenantContext(tenantRecordId, accountRecordId, fixedOffsetTimeZone, referenceTime);
         }
     }
 
@@ -222,8 +243,23 @@ public class InternalCallContextFactory {
 
     // Used when we need to re-hydrate the callcontext with the account_record_id (when creating the account)
     public InternalCallContext createInternalCallContext(final Long accountRecordId, final InternalCallContext context) {
-        final DateTimeZone fixedOffsetTimeZone = getFixedOffsetTimeZone(accountRecordId, context.getTenantRecordId());
-        final DateTime referenceTime = getReferenceTime(accountRecordId, context.getTenantRecordId());
+        final ImmutableAccountData immutableAccountData = getImmutableAccountData(accountRecordId, context.getTenantRecordId());
+        final DateTimeZone fixedOffsetTimeZone = immutableAccountData.getFixedOffsetTimeZone();
+        final DateTime referenceTime = immutableAccountData.getReferenceTime();
+        populateMDCContext(accountRecordId, context.getTenantRecordId());
+        return new InternalCallContext(context, accountRecordId, fixedOffsetTimeZone, referenceTime, clock.getUTCNow());
+    }
+
+    // Used during the account creation transaction (account not visible outside of the transaction yet)
+    public InternalCallContext createInternalCallContext(final TimeZoneAwareEntity accountModelDao, final Long accountRecordId, final InternalCallContext context) {
+        // See DefaultImmutableAccountData implementation
+        final DateTimeZone fixedOffsetTimeZone = AccountDateTimeUtils.getFixedOffsetTimeZone(accountModelDao);
+        final DateTime referenceTime = AccountDateTimeUtils.getReferenceDateTime(accountModelDao);
+        populateMDCContext(accountRecordId, context.getTenantRecordId());
+        return new InternalCallContext(context, accountRecordId, fixedOffsetTimeZone, referenceTime, clock.getUTCNow());
+    }
+
+    public InternalCallContext createInternalCallContext(final DateTimeZone fixedOffsetTimeZone, final DateTime referenceTime, final Long accountRecordId, final InternalCallContext context) {
         populateMDCContext(accountRecordId, context.getTenantRecordId());
         return new InternalCallContext(context, accountRecordId, fixedOffsetTimeZone, referenceTime, clock.getUTCNow());
     }
@@ -242,8 +278,18 @@ public class InternalCallContextFactory {
                                                           final CallOrigin callOrigin, final UserType userType, @Nullable final UUID userToken,
                                                           @Nullable final String reasonCode, @Nullable final String comment) {
         final Long nonNulTenantRecordId = MoreObjects.firstNonNull(tenantRecordId, INTERNAL_TENANT_RECORD_ID);
-        final DateTimeZone fixedOffsetTimeZone = getFixedOffsetTimeZone(accountRecordId, tenantRecordId);
-        final DateTime referenceTime = getReferenceTime(accountRecordId, tenantRecordId);
+
+        final DateTimeZone fixedOffsetTimeZone;
+        final DateTime referenceTime;
+        if (accountRecordId == null) {
+            // TENANT_CONFIG_CHANGE event for instance
+            fixedOffsetTimeZone = null;
+            referenceTime = null;
+        } else {
+            final ImmutableAccountData immutableAccountData = getImmutableAccountData(accountRecordId, nonNulTenantRecordId);
+            fixedOffsetTimeZone = immutableAccountData.getFixedOffsetTimeZone();
+            referenceTime = immutableAccountData.getReferenceTime();
+        }
 
         populateMDCContext(accountRecordId, nonNulTenantRecordId);
 
@@ -261,34 +307,15 @@ public class InternalCallContextFactory {
                                        clock.getUTCNow());
     }
 
-    private DateTimeZone getFixedOffsetTimeZone(@Nullable final Long accountRecordId, final Long tenantRecordId) {
-        if (accountRecordId == null || accountInternalApi == null) {
-            return null;
-        }
-
-        populateMDCContext(accountRecordId, tenantRecordId);
-
-        final ImmutableAccountData immutableAccountData = getImmutableAccountData(accountRecordId, tenantRecordId);
-        // Will be null while creating the account
-        return immutableAccountData == null ? null : immutableAccountData.getFixedOffsetTimeZone();
-    }
-
-    private DateTime getReferenceTime(@Nullable final Long accountRecordId, final Long tenantRecordId) {
-        if (accountRecordId == null || accountInternalApi == null) {
-            return null;
-        }
-
-        final ImmutableAccountData immutableAccountData = getImmutableAccountData(accountRecordId, tenantRecordId);
-        // Will be null while creating the account
-        return immutableAccountData == null ? null : immutableAccountData.getReferenceTime();
-    }
-
-    private ImmutableAccountData getImmutableAccountData(@Nullable final Long accountRecordId, final Long tenantRecordId) {
+    private ImmutableAccountData getImmutableAccountData(final Long accountRecordId, final Long tenantRecordId) {
+        Preconditions.checkNotNull(accountRecordId, "Missing accountRecordId");
         final InternalTenantContext tmp = new InternalTenantContext(tenantRecordId, accountRecordId, null, null);
         try {
-            return accountInternalApi.getImmutableAccountDataByRecordId(accountRecordId, tmp);
+            final ImmutableAccountData immutableAccountData = accountInternalApi.getImmutableAccountDataByRecordId(accountRecordId, tmp);
+            Preconditions.checkNotNull(immutableAccountData, "Unable to retrieve immutableAccountData");
+            return immutableAccountData;
         } catch (final AccountApiException e) {
-            return null;
+            throw new RuntimeException(e);
         }
     }
 
@@ -307,7 +334,7 @@ public class InternalCallContextFactory {
     public UUID getAccountId(final UUID objectId, final ObjectType objectType, final TenantContext context) {
         final Long accountRecordId = getAccountRecordIdSafe(objectId, objectType, context);
         if (accountRecordId != null) {
-            return nonEntityDao.retrieveIdFromObject(accountRecordId, ObjectType.ACCOUNT, cacheControllerDispatcher == null ? null : cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+            return nonEntityDao.retrieveIdFromObject(accountRecordId, ObjectType.ACCOUNT, objectIdCacheController);
         } else {
             return null;
         }
@@ -317,7 +344,7 @@ public class InternalCallContextFactory {
     public Long getRecordIdFromObject(final UUID objectId, final ObjectType objectType, final TenantContext context) {
         try {
             if (objectBelongsToTheRightTenant(objectId, objectType, context)) {
-                return nonEntityDao.retrieveRecordIdFromObject(objectId, objectType, cacheControllerDispatcher == null ? null : cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID));
+                return nonEntityDao.retrieveRecordIdFromObject(objectId, objectType, recordIdCacheController);
             } else {
                 return null;
             }
@@ -358,7 +385,7 @@ public class InternalCallContextFactory {
     }
 
     private UUID getTenantIdSafe(final InternalTenantContext context) {
-        return nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, cacheControllerDispatcher == null ? null : cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID));
+        return nonEntityDao.retrieveIdFromObject(context.getTenantRecordId(), ObjectType.TENANT, objectIdCacheController);
     }
 
     //
@@ -386,11 +413,11 @@ public class InternalCallContextFactory {
     //
 
     private Long getAccountRecordIdUnsafe(final UUID objectId, final ObjectType objectType) {
-        return nonEntityDao.retrieveAccountRecordIdFromObject(objectId, objectType, cacheControllerDispatcher == null ? null : cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID));
+        return nonEntityDao.retrieveAccountRecordIdFromObject(objectId, objectType, accountRecordIdCacheController);
     }
 
     private Long getTenantRecordIdUnsafe(final UUID objectId, final ObjectType objectType) {
-        return nonEntityDao.retrieveTenantRecordIdFromObject(objectId, objectType, cacheControllerDispatcher == null ? null : cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID));
+        return nonEntityDao.retrieveTenantRecordIdFromObject(objectId, objectType, tenantRecordIdCacheController);
     }
 
     public static final class ObjectDoesNotExist extends IllegalStateException {
diff --git a/util/src/main/java/org/killbill/billing/util/config/tenant/CacheConfig.java b/util/src/main/java/org/killbill/billing/util/config/tenant/CacheConfig.java
index 1338b65..789cc0b 100644
--- a/util/src/main/java/org/killbill/billing/util/config/tenant/CacheConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/tenant/CacheConfig.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -34,7 +34,7 @@ import com.google.inject.Inject;
 
 public class CacheConfig {
 
-    private final CacheController cacheController;
+    private final CacheController<Long, PerTenantConfig> cacheController;
     private final CacheLoaderArgument cacheLoaderArgument;
 
     private final ObjectMapper objectMapper;
@@ -48,7 +48,7 @@ public class CacheConfig {
     }
 
     public PerTenantConfig getPerTenantConfig(final InternalTenantContext tenantContext) {
-        final PerTenantConfig perTenantConfig = (PerTenantConfig) cacheController.get(tenantContext.getTenantRecordId(), cacheLoaderArgument);
+        final PerTenantConfig perTenantConfig = cacheController.get(tenantContext.getTenantRecordId(), cacheLoaderArgument);
         return perTenantConfig;
     }
 
@@ -59,7 +59,7 @@ public class CacheConfig {
     private CacheLoaderArgument initializeCacheLoaderArgument() {
         final LoaderCallback loaderCallback = new LoaderCallback() {
             @Override
-            public Object loadConfig(@Nullable final String inputJson) throws IOException {
+            public PerTenantConfig loadConfig(@Nullable final String inputJson) throws IOException {
                 return inputJson != null ? objectMapper.readValue(inputJson, PerTenantConfig.class) : new PerTenantConfig();
             }
         };
@@ -69,5 +69,4 @@ public class CacheConfig {
         final InternalTenantContext notUsed = null;
         return new CacheLoaderArgument(irrelevant, args, notUsed);
     }
-
 }
diff --git a/util/src/main/java/org/killbill/billing/util/config/tenant/PerTenantConfig.java b/util/src/main/java/org/killbill/billing/util/config/tenant/PerTenantConfig.java
index 0d04b4a..25a3f34 100644
--- a/util/src/main/java/org/killbill/billing/util/config/tenant/PerTenantConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/tenant/PerTenantConfig.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -17,11 +17,30 @@
 
 package org.killbill.billing.util.config.tenant;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
 import java.util.HashMap;
 
-public class PerTenantConfig extends HashMap<String, String> {
+import org.killbill.billing.util.cache.ExternalizableInput;
+import org.killbill.billing.util.cache.ExternalizableOutput;
+import org.killbill.billing.util.cache.MapperHolder;
+
+public class PerTenantConfig extends HashMap<String, String> implements Externalizable {
+
+    private static final long serialVersionUID = 3887971108446630172L;
 
     public PerTenantConfig() {
     }
 
+    @Override
+    public void readExternal(final ObjectInput in) throws IOException {
+        MapperHolder.mapper().readerForUpdating(this).readValue(new ExternalizableInput(in));
+    }
+
+    @Override
+    public void writeExternal(final ObjectOutput oo) throws IOException {
+        MapperHolder.mapper().writeValue(new ExternalizableOutput(oo), this);
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java
index 30cee0c..7ca13e0 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/AuditSqlDao.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -36,7 +38,7 @@ import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 
 /**
  * Note 1: cache invalidation has to happen for audit logs (which is tricky in the multi-nodes scenario).
- * For now, we're using a time-based eviction strategy (see timeToIdleSeconds and timeToLiveSeconds in ehcache.xml)
+ * For now, we're using a time-based eviction strategy (see ehcache.xml)
  * which is good enough: the cache will always get at least the initial CREATION audit log entry, which is the one
  * we really care about (both for Analytics and for Kaui's endpoints). Besides, we do cache invalidation properly
  * on our own node (see EntitySqlDaoWrapperInvocationHandler).
diff --git a/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java b/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java
index 043805e..7cec9b9 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/DefaultNonEntityDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -40,22 +40,22 @@ import com.google.common.base.Preconditions;
 public class DefaultNonEntityDao implements NonEntityDao {
 
     private final NonEntitySqlDao nonEntitySqlDao;
-    private final WithCaching<UUID, Long> withCachingObjectId;
-    private final WithCaching<Long, UUID> withCachingRecordId;
+    private final WithCaching<String, Long> withCachingObjectId;
+    private final WithCaching<String, UUID> withCachingRecordId;
 
     @Inject
     public DefaultNonEntityDao(final IDBI dbi) {
         this.nonEntitySqlDao = dbi.onDemand(NonEntitySqlDao.class);
-        this.withCachingObjectId = new WithCaching<UUID, Long>();
-        this.withCachingRecordId = new WithCaching<Long, UUID>();
+        this.withCachingObjectId = new WithCaching<String, Long>();
+        this.withCachingRecordId = new WithCaching<String, UUID>();
     }
 
     @Override
-    public Long retrieveRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return retrieveRecordIdFromObjectInTransaction(objectId, objectType, cache, null);
     }
 
-    public Long retrieveRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         if (objectId == null) {
             return null;
         }
@@ -63,28 +63,29 @@ public class DefaultNonEntityDao implements NonEntityDao {
         final TableName tableName = TableName.fromObjectType(objectType);
         Preconditions.checkNotNull(tableName, "%s is not a valid ObjectType", objectType);
 
-        return withCachingObjectId.withCaching(new OperationRetrieval<UUID, Long>() {
+        return withCachingObjectId.withCaching(new OperationRetrieval<Long>() {
             @Override
-            public Long doRetrieve(final UUID objectOrRecordId, final ObjectType objectType) {
+            public Long doRetrieve(final ObjectType objectType) {
                 final NonEntitySqlDao inTransactionNonEntitySqlDao = handle == null ? nonEntitySqlDao : SqlObjectBuilder.attach(handle, NonEntitySqlDao.class);
                 return inTransactionNonEntitySqlDao.getRecordIdFromObject(objectId.toString(), tableName.getTableName());
             }
-        }, objectId, objectType, tableName, cache);
+        }, objectId.toString(), objectType, tableName, cache);
     }
 
     @Override
-    public Long retrieveAccountRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveAccountRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return retrieveAccountRecordIdFromObjectInTransaction(objectId, objectType, cache, null);
     }
 
     @Override
-    public Long retrieveAccountRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveAccountRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         final TableName tableName = TableName.fromObjectType(objectType);
         Preconditions.checkNotNull(tableName, "%s is not a valid ObjectType", objectType);
 
-        return withCachingObjectId.withCaching(new OperationRetrieval<UUID, Long>() {
+        final String objectIdOrNull = objectId != null ? objectId.toString() : null;
+        return withCachingObjectId.withCaching(new OperationRetrieval<Long>() {
             @Override
-            public Long doRetrieve(final UUID objectId, final ObjectType objectType) {
+            public Long doRetrieve(final ObjectType objectType) {
                 final NonEntitySqlDao inTransactionNonEntitySqlDao = handle == null ? nonEntitySqlDao : SqlObjectBuilder.attach(handle, NonEntitySqlDao.class);
 
                 switch (tableName) {
@@ -94,64 +95,65 @@ public class DefaultNonEntityDao implements NonEntityDao {
                         return null;
 
                     case ACCOUNT:
-                        return inTransactionNonEntitySqlDao.getAccountRecordIdFromAccount(objectId.toString());
+                        return inTransactionNonEntitySqlDao.getAccountRecordIdFromAccount(objectIdOrNull);
 
                     default:
-                        return inTransactionNonEntitySqlDao.getAccountRecordIdFromObjectOtherThanAccount(objectId.toString(), tableName.getTableName());
+                        return inTransactionNonEntitySqlDao.getAccountRecordIdFromObjectOtherThanAccount(objectIdOrNull, tableName.getTableName());
                 }
             }
-        }, objectId, objectType, tableName, cache);
+        }, objectIdOrNull, objectType, tableName, cache);
     }
 
     @Override
-    public Long retrieveTenantRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveTenantRecordIdFromObject(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return retrieveTenantRecordIdFromObjectInTransaction(objectId, objectType, cache, null);
     }
 
     @Override
-    public Long retrieveTenantRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveTenantRecordIdFromObjectInTransaction(@Nullable final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         final TableName tableName = TableName.fromObjectType(objectType);
         Preconditions.checkNotNull(tableName, "%s is not a valid ObjectType", objectType);
 
-        return withCachingObjectId.withCaching(new OperationRetrieval<UUID, Long>() {
+        final String objectIdOrNull = objectId != null ? objectId.toString() : null;
+        return withCachingObjectId.withCaching(new OperationRetrieval<Long>() {
             @Override
-            public Long doRetrieve(final UUID objectId, final ObjectType objectType) {
+            public Long doRetrieve(final ObjectType objectType) {
                 final NonEntitySqlDao inTransactionNonEntitySqlDao = handle == null ? nonEntitySqlDao : SqlObjectBuilder.attach(handle, NonEntitySqlDao.class);
 
                 switch (tableName) {
                     case TENANT:
                         // Explicit cast to Long to avoid NPE (unboxing to long)
-                        return objectId == null ? (Long) 0L : inTransactionNonEntitySqlDao.getTenantRecordIdFromTenant(objectId.toString());
+                        return objectId == null ? (Long) 0L : inTransactionNonEntitySqlDao.getTenantRecordIdFromTenant(objectIdOrNull);
 
                     default:
-                        return inTransactionNonEntitySqlDao.getTenantRecordIdFromObjectOtherThanTenant(objectId.toString(), tableName.getTableName());
+                        return inTransactionNonEntitySqlDao.getTenantRecordIdFromObjectOtherThanTenant(objectIdOrNull, tableName.getTableName());
                 }
 
             }
-        }, objectId, objectType, tableName, cache);
+        }, objectIdOrNull, objectType, tableName, cache);
     }
 
     @Override
-    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache) {
         return retrieveIdFromObjectInTransaction(recordId, objectType, cache, null);
     }
 
     @Override
-    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
-        if (objectType == ObjectType.TENANT && recordId == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache, @Nullable final Handle handle) {
+        if (objectType == ObjectType.TENANT && InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID.equals(recordId)) {
             return null;
         }
 
         final TableName tableName = TableName.fromObjectType(objectType);
         Preconditions.checkNotNull(tableName, "%s is not a valid ObjectType", objectType);
 
-        return withCachingRecordId.withCaching(new OperationRetrieval<Long, UUID>() {
+        return withCachingRecordId.withCaching(new OperationRetrieval<UUID>() {
             @Override
-            public UUID doRetrieve(final Long objectOrRecordId, final ObjectType objectType) {
+            public UUID doRetrieve(final ObjectType objectType) {
                 final NonEntitySqlDao inTransactionNonEntitySqlDao = handle == null ? nonEntitySqlDao : SqlObjectBuilder.attach(handle, NonEntitySqlDao.class);
                 return inTransactionNonEntitySqlDao.getIdFromObject(recordId, tableName.getTableName());
             }
-        }, recordId, objectType, tableName, cache);
+        }, String.valueOf(recordId), objectType, tableName, cache);
     }
 
     @Override
@@ -165,31 +167,31 @@ public class DefaultNonEntityDao implements NonEntityDao {
         return nonEntitySqlDao.getHistoryTargetRecordId(recordId, tableName.getTableName());
     }
 
-    private interface OperationRetrieval<TypeIn, TypeOut> {
+    private interface OperationRetrieval<TypeOut> {
 
-        public TypeOut doRetrieve(final TypeIn objectOrRecordId, final ObjectType objectType);
+        public TypeOut doRetrieve(final ObjectType objectType);
     }
 
     // 'cache' will be null for the CacheLoader classes -- or if cache is not configured.
     private class WithCaching<TypeIn, TypeOut> {
 
-        private TypeOut withCaching(final OperationRetrieval<TypeIn, TypeOut> op, @Nullable final TypeIn objectOrRecordId, final ObjectType objectType, final TableName tableName, @Nullable final CacheController<Object, Object> cache) {
+        private TypeOut withCaching(final OperationRetrieval<TypeOut> op, @Nullable final TypeIn objectOrRecordId, final ObjectType objectType, final TableName tableName, @Nullable final CacheController<TypeIn, TypeOut> cache) {
 
             final Profiling<TypeOut, RuntimeException> prof = new Profiling<TypeOut, RuntimeException>();
             if (objectOrRecordId == null) {
                 return null;
             }
             if (cache != null) {
-                final String key = (cache.getCacheType().isKeyPrefixedWithTableName()) ?
-                                   tableName + CacheControllerDispatcher.CACHE_KEY_SEPARATOR + objectOrRecordId.toString() :
-                                   objectOrRecordId.toString();
-                return (TypeOut) cache.get(key, new CacheLoaderArgument(objectType));
+                final TypeIn key = (cache.getCacheType().isKeyPrefixedWithTableName()) ?
+                                   (TypeIn) (tableName + CacheControllerDispatcher.CACHE_KEY_SEPARATOR + objectOrRecordId.toString()) :
+                                   objectOrRecordId;
+                return cache.get(key, new CacheLoaderArgument(objectType));
             }
             final TypeOut result;
             result = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, "NonEntityDao (type = " + objectType + ") cache miss", new WithProfilingCallback<TypeOut, RuntimeException>() {
                 @Override
                 public TypeOut execute() throws RuntimeException {
-                    return op.doRetrieve(objectOrRecordId, objectType);
+                    return op.doRetrieve(objectType);
                 }
             });
             return result;
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java
new file mode 100644
index 0000000..5ff331d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.util.audit.ChangeType;
+import org.killbill.billing.util.entity.Entity;
+import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.killbill.billing.util.entity.dao.EntityModelDaoBase;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class EntityHistoryModelDaoMapper<M extends EntityModelDao<E>, E extends Entity> extends MapperBase implements ResultSetMapper<EntityHistoryModelDao<M, E>> {
+
+    private final ResultSetMapper<M> entityMapper;
+
+    public EntityHistoryModelDaoMapper(final ResultSetMapper<M> entityMapper) {
+        this.entityMapper = entityMapper;
+    }
+
+    @Override
+    public EntityHistoryModelDao<M, E> map(final int index, final ResultSet r, final StatementContext ctx) throws SQLException {
+        final UUID id = getUUID(r, "id");
+        final long targetRecordId = r.getLong("target_record_id");
+        final String changeType = r.getString("change_type");
+        final DateTime createdDate = getDateTime(r, "created_date");
+
+        final M entityModelDao = entityMapper.map(index, r, ctx);
+
+        // Hack -- remove the id as it is the history id, not the entity id
+        ((EntityModelDaoBase) entityModelDao).setId(null);
+        // Hack -- similarly, populate the right record_id
+        ((EntityModelDaoBase) entityModelDao).setRecordId(targetRecordId);
+        // Hack -- account is special
+        if (entityModelDao.getAccountRecordId() == null) {
+            ((EntityModelDaoBase) entityModelDao).setAccountRecordId(targetRecordId);
+        }
+
+        return new EntityHistoryModelDao(id, entityModelDao, targetRecordId, ChangeType.valueOf(changeType), createdDate);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java
new file mode 100644
index 0000000..38cc98d
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/dao/EntityHistoryModelDaoMapperFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.dao;
+
+import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapper;
+import org.skife.jdbi.v2.ResultSetMapperFactory;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.ResultSetMapper;
+
+public class EntityHistoryModelDaoMapperFactory implements ResultSetMapperFactory {
+
+    private final Class<?> sqlObjectType;
+    private final Class<?> modelClazz;
+
+    public EntityHistoryModelDaoMapperFactory(final Class modelClazz, final Class<?> sqlObjectType) {
+        this.sqlObjectType = sqlObjectType;
+        this.modelClazz = modelClazz;
+    }
+
+    @Override
+    public boolean accepts(final Class type, final StatementContext ctx) {
+        return type.isAssignableFrom(EntityHistoryModelDao.class) && sqlObjectType.equals(ctx.getSqlObjectType());
+    }
+
+    @Override
+    public ResultSetMapper mapperFor(final Class type, final StatementContext ctx) {
+        return new EntityHistoryModelDaoMapper(new LowerToCamelBeanMapper(modelClazz));
+    }
+}
+
diff --git a/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
index c829280..5f6d2d1 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/HistorySqlDao.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -16,16 +18,24 @@
 
 package org.killbill.billing.util.dao;
 
-import org.skife.jdbi.v2.sqlobject.BindBean;
-import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import java.util.List;
 
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.entity.dao.EntityModelDao;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.GetGeneratedKeys;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 
 public interface HistorySqlDao<M extends EntityModelDao<E>, E extends Entity> {
 
+    @SqlQuery
+    public List<EntityHistoryModelDao<M, E>> getHistoryForTargetRecordId(@Bind("targetRecordId") final long targetRecordId,
+                                                                         @BindBean InternalCallContext context);
     @SqlUpdate
-    public void addHistoryFromTransaction(@EntityHistoryBinder EntityHistoryModelDao<M, E> history,
+    @GetGeneratedKeys
+    public Long addHistoryFromTransaction(@EntityHistoryBinder EntityHistoryModelDao<M, E> history,
                                           @BindBean InternalCallContext context);
 }
diff --git a/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java b/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java
index 72c9cf2..0261192 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/NonEntityDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -29,21 +29,21 @@ import org.skife.jdbi.v2.Handle;
 // This should only be used for internal operations (trusted code, not API), because the context will not be validated!
 public interface NonEntityDao {
 
-    public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+    public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache);
 
-    public Long retrieveRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle);
+    public Long retrieveRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle);
 
-    public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+    public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache);
 
-    public Long retrieveAccountRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle);
+    public Long retrieveAccountRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle);
 
-    public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+    public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache);
 
-    public Long retrieveTenantRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle);
+    public Long retrieveTenantRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle);
 
-    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache);
+    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache);
 
-    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle);
+    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache, @Nullable final Handle handle);
 
     // This retrieves from the history table the latest record for which targetId matches the one we are passing
     public Long retrieveLastHistoryRecordIdFromTransaction(final Long targetRecordId, final TableName tableName, final NonEntitySqlDao transactional);
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java
index 32ad513..f06589f 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntityDaoBase.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -22,6 +24,7 @@ import java.util.UUID;
 import org.killbill.billing.BillingExceptionBase;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.entity.EntityPersistenceException;
 import org.killbill.billing.util.audit.ChangeType;
 import org.killbill.billing.util.entity.DefaultPagination;
 import org.killbill.billing.util.entity.Entity;
@@ -44,30 +47,35 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
 
     @Override
     public void create(final M entity, final InternalCallContext context) throws U {
-        transactionalSqlDao.execute(getCreateEntitySqlDaoTransactionWrapper(entity, context));
+        final M refreshedEntity = transactionalSqlDao.execute(getCreateEntitySqlDaoTransactionWrapper(entity, context));
+        // Populate the caches only after the transaction has been committed, in case of rollbacks
+        transactionalSqlDao.populateCaches(refreshedEntity);
     }
 
-    protected EntitySqlDaoTransactionWrapper<Void> getCreateEntitySqlDaoTransactionWrapper(final M entity, final InternalCallContext context) {
-        return new EntitySqlDaoTransactionWrapper<Void>() {
+    protected EntitySqlDaoTransactionWrapper<M> getCreateEntitySqlDaoTransactionWrapper(final M entity, final InternalCallContext context) {
+        return new EntitySqlDaoTransactionWrapper<M>() {
             @Override
-            public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+            public M inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
                 final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
 
                 if (checkEntityAlreadyExists(transactional, entity, context)) {
                     throw generateAlreadyExistsException(entity, context);
                 }
-                transactional.create(entity, context);
-
-                final M refreshedEntity = transactional.getById(entity.getId().toString(), context);
+                final M refreshedEntity = createAndRefresh(transactional, entity, context);
 
                 postBusEventFromTransaction(entity, refreshedEntity, ChangeType.INSERT, entitySqlDaoWrapperFactory, context);
-                return null;
+                return refreshedEntity;
             }
         };
     }
 
+    protected <F extends EntityModelDao> F createAndRefresh(final EntitySqlDao transactional, final F entity, final InternalCallContext context) throws EntityPersistenceException {
+        // We have overridden the jDBI return type in EntitySqlDaoWrapperInvocationHandler
+        return (F) transactional.create(entity, context);
+    }
+
     protected boolean checkEntityAlreadyExists(final EntitySqlDao<M, E> transactional, final M entity, final InternalCallContext context) {
-        return transactional.getById(entity.getId().toString(), context) != null;
+        return transactional.getRecordId(entity.getId().toString(), context) != null;
     }
 
     protected void postBusEventFromTransaction(final M entity, final M savedEntity, final ChangeType changeType,
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java
index d20a5e7..ede762c 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDao.java
@@ -45,8 +45,8 @@ public interface EntitySqlDao<M extends EntityModelDao<E>, E extends Entity> ext
 
     @SqlUpdate
     @Audited(ChangeType.INSERT)
-    public void create(@BindBean final M entity,
-                       @BindBean final InternalCallContext context) throws EntityPersistenceException;
+    public Object create(@BindBean final M entity,
+                         @BindBean final InternalCallContext context) throws EntityPersistenceException;
 
     @SqlQuery
     public M getById(@Bind("id") final String id,
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
index 12d9517..ce2fd8d 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoStringTemplate.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -28,6 +30,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.regex.Matcher;
 
+import org.killbill.billing.util.dao.EntityHistoryModelDaoMapperFactory;
+import org.killbill.billing.util.entity.Entity;
 import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
 import org.skife.jdbi.v2.Query;
 import org.skife.jdbi.v2.SQLStatement;
@@ -38,8 +42,6 @@ import org.skife.jdbi.v2.sqlobject.stringtemplate.StringTemplate3StatementLocato
 import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 import org.skife.jdbi.v2.tweak.StatementLocator;
 
-import org.killbill.billing.util.entity.Entity;
-
 @SqlStatementCustomizingAnnotation(EntitySqlDaoStringTemplate.EntitySqlDaoLocatorFactory.class)
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE})
@@ -114,6 +116,7 @@ public @interface EntitySqlDaoStringTemplate {
                                             final Class modelClazz = (Class) modelType;
                                             if (Entity.class.isAssignableFrom(modelClazz)) {
                                                 query.registerMapper(new LowerToCamelBeanMapperFactory(modelClazz));
+                                                query.registerMapper(new EntityHistoryModelDaoMapperFactory(modelClazz, sqlObjectType));
                                             }
                                         }
                                     }
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
index e70fcba..b16601d 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
@@ -49,6 +49,10 @@ public class EntitySqlDaoTransactionalJdbiWrapper {
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
+    public <M extends EntityModelDao> void populateCaches(final M refreshedEntity) {
+        EntitySqlDaoWrapperInvocationHandler.populateCaches(cacheControllerDispatcher, refreshedEntity);
+    }
+
     class JdbiTransaction<ReturnType, M extends EntityModelDao<E>, E extends Entity> implements Transaction<ReturnType, EntitySqlDao<M, E>> {
 
         private final Handle h;
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java
index 872ba10..654d0da 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/EntitySqlDaoWrapperInvocationHandler.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2012 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -21,6 +21,7 @@ package org.killbill.billing.util.entity.dao;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
@@ -30,6 +31,7 @@ import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import javax.annotation.Nullable;
 
@@ -47,7 +49,6 @@ import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.EntityAudit;
 import org.killbill.billing.util.dao.EntityHistoryModelDao;
 import org.killbill.billing.util.dao.NonEntityDao;
-import org.killbill.billing.util.dao.NonEntitySqlDao;
 import org.killbill.billing.util.dao.TableName;
 import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.tag.dao.UUIDCollectionBinder;
@@ -61,11 +62,11 @@ import org.skife.jdbi.v2.StatementContext;
 import org.skife.jdbi.v2.exceptions.DBIException;
 import org.skife.jdbi.v2.exceptions.StatementException;
 import org.skife.jdbi.v2.sqlobject.Bind;
-import org.skife.jdbi.v2.sqlobject.SqlObjectBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
@@ -91,7 +92,7 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
     private final Clock clock;
     private final NonEntityDao nonEntityDao;
     private final InternalCallContextFactory internalCallContextFactory;
-    private final Profiling prof;
+    private final Profiling<Object, Throwable> prof;
 
     public EntitySqlDaoWrapperInvocationHandler(final Class<S> sqlDaoClass,
                                                 final S sqlDao,
@@ -114,7 +115,7 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
     @Override
     public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
         try {
-            return prof.executeWithProfiling(ProfilingFeatureType.DAO, sqlDaoClass.getSimpleName() + ":" + method.getName(), new WithProfilingCallback() {
+            return prof.executeWithProfiling(ProfilingFeatureType.DAO, getProfilingId(null, method), new WithProfilingCallback<Object, Throwable>() {
                 @Override
                 public Object execute() throws Throwable {
                     return invokeSafely(proxy, method, args);
@@ -201,7 +202,7 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
     }
 
     private Object invokeRaw(final Method method, final Object[] args) throws Throwable {
-        return prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, sqlDaoClass.getSimpleName() + " (raw):" + method.getName(), new WithProfilingCallback() {
+        return prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, getProfilingId("raw", method), new WithProfilingCallback<Object, Throwable>() {
             @Override
             public Object execute() throws Throwable {
                 Object result = method.invoke(sqlDao, args);
@@ -222,8 +223,8 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
         final ObjectType objectType = getObjectType();
         final CacheType cacheType = cachableAnnotation.value();
         final CacheController<Object, Object> cache = cacheControllerDispatcher.getCacheController(cacheType);
-        Object result = null;
-        if (cache != null) {
+        // TODO Change NonEntityDao to take in TableName instead to cache things like TenantBroadcastModelDao (no ObjectType)
+        if (cache != null && objectType != null) {
             // Find all arguments marked with @CachableKey
             final Map<Integer, Object> keyPieces = new LinkedHashMap<Integer, Object>();
             final Annotation[][] annotations = method.getParameterAnnotations();
@@ -249,16 +250,9 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
             }, null);
             final CacheLoaderArgument cacheLoaderArgument = new CacheLoaderArgument(objectType, args, internalTenantContext, handle);
             return cache.get(cacheKey, cacheLoaderArgument);
+        } else {
+            return invokeRaw(method, args);
         }
-        if (result == null) {
-            result = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, sqlDaoClass.getSimpleName() + "(raw) :" + method.getName(), new WithProfilingCallback() {
-                @Override
-                public Object execute() throws Throwable {
-                    return method.invoke(sqlDao, args);
-                }
-            });
-        }
-        return result;
     }
 
     /**
@@ -292,9 +286,14 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
             int foundIndexForEntityModelDao = -1;
             for (int i = 0; i < types.length; i++) {
                 final Class clz = ((Class) types[i]);
-                if (EntityModelDao.class.getName().equals(((Class) ((java.lang.reflect.ParameterizedType) clz.getGenericInterfaces()[0]).getRawType()).getName())) {
-                    foundIndexForEntityModelDao = i;
-                    break;
+                final Type[] genericInterfaces = clz.getGenericInterfaces();
+                for (final Type genericInterface : genericInterfaces) {
+                    if (genericInterface instanceof ParameterizedType) {
+                        if (EntityModelDao.class.getName().equals(((Class) ((ParameterizedType) genericInterface).getRawType()).getName())) {
+                            foundIndexForEntityModelDao = i;
+                            break;
+                        }
+                    }
                 }
             }
 
@@ -311,87 +310,101 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
     }
 
     private Object invokeWithAuditAndHistory(final Audited auditedAnnotation, final Method method, final Object[] args) throws Throwable {
-        InternalCallContext context = null;
-        List<String> entityIds = null;
-        final Map<String, M> entities = new HashMap<String, M>();
-        final Map<String, Long> entityRecordIds = new HashMap<String, Long>();
-        if (auditedAnnotation != null) {
-            // There will be some work required after the statement is executed,
-            // get the id before in case the change is a delete
-            context = retrieveContextFromArguments(args);
-            entityIds = retrieveEntityIdsFromArguments(method, args);
+        final InternalCallContext context = retrieveContextFromArguments(args);
+        final List<String> entityIds = retrieveEntityIdsFromArguments(method, args);
+
+        final ChangeType changeType = auditedAnnotation.value();
+
+        // Get the current state before deletion for the history tables
+        final Map<String, M> deletedEntities = new HashMap<String, M>();
+        // Unfortunately, we cannot just look at DELETE as "markAsInactive" operations are often treated as UPDATE
+        if (changeType == ChangeType.UPDATE || changeType == ChangeType.DELETE) {
             for (final String entityId : entityIds) {
-                entities.put(entityId, sqlDao.getById(entityId, context));
-                entityRecordIds.put(entityId, sqlDao.getRecordId(entityId, context));
+                deletedEntities.put(entityId, sqlDao.getById(entityId, context));
             }
         }
 
         // Real jdbc call
-        final Object obj = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, sqlDaoClass.getSimpleName() + " (raw) :", new WithProfilingCallback() {
+        final Object obj = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, getProfilingId("raw", method), new WithProfilingCallback<Object, Throwable>() {
             @Override
             public Object execute() throws Throwable {
                 return method.invoke(sqlDao, args);
             }
         });
 
-        final ChangeType changeType = auditedAnnotation.value();
-
+        M m = null;
         for (final String entityId : entityIds) {
-            updateHistoryAndAudit(entityId, entities, entityRecordIds, changeType, context);
+            m = updateHistoryAndAudit(entityId, deletedEntities.get(entityId), changeType, context);
+        }
+
+        // PERF: override the return value with the reHydrated entity to avoid an extra 'get' in the transaction,
+        // (see EntityDaoBase#createAndRefresh for an example, but it works for updates as well).
+        if (entityIds.size() == 1) {
+            return m;
+        } else {
+            // jDBI will return the number of rows modified otherwise
+            return obj;
         }
-        return obj;
     }
 
-    private void populateCacheOnGetByIdInvocation(M model) {
+    private void populateCacheOnGetByIdInvocation(final M model) {
+        populateCaches(cacheControllerDispatcher, model);
+    }
 
-        final CacheController<Object, Object> cacheRecordId = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
-        cacheRecordId.add(getKey(model.getId().toString(), CacheType.RECORD_ID, model.getTableName()), model.getRecordId());
+    public static void populateCaches(final CacheControllerDispatcher cacheControllerDispatcher, final EntityModelDao model) {
+        final CacheController<String, Long> cacheRecordId = cacheControllerDispatcher.getCacheController(CacheType.RECORD_ID);
+        cacheRecordId.putIfAbsent(getKey(model.getId().toString(), CacheType.RECORD_ID, model.getTableName()), model.getRecordId());
 
-        final CacheController<Object, Object> cacheObjectId = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
-        cacheObjectId.add(getKey(model.getRecordId().toString(), CacheType.OBJECT_ID, model.getTableName()), model.getId());
+        final CacheController<String, UUID> cacheObjectId = cacheControllerDispatcher.getCacheController(CacheType.OBJECT_ID);
+        cacheObjectId.putIfAbsent(getKey(model.getRecordId().toString(), CacheType.OBJECT_ID, model.getTableName()), model.getId());
 
         if (model.getTenantRecordId() != null) {
-            final CacheController<Object, Object> cacheTenantRecordId = cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID);
-            cacheTenantRecordId.add(getKey(model.getId().toString(), CacheType.TENANT_RECORD_ID, model.getTableName()), model.getTenantRecordId());
+            final CacheController<String, Long> cacheTenantRecordId = cacheControllerDispatcher.getCacheController(CacheType.TENANT_RECORD_ID);
+            cacheTenantRecordId.putIfAbsent(getKey(model.getId().toString(), CacheType.TENANT_RECORD_ID, model.getTableName()), model.getTenantRecordId());
         }
 
         if (model.getAccountRecordId() != null) {
-            final CacheController<Object, Object> cacheAccountRecordId = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID);
-            cacheAccountRecordId.add(getKey(model.getId().toString(), CacheType.ACCOUNT_RECORD_ID, model.getTableName()), model.getAccountRecordId());
+            final CacheController<String, Long> cacheAccountRecordId = cacheControllerDispatcher.getCacheController(CacheType.ACCOUNT_RECORD_ID);
+            cacheAccountRecordId.putIfAbsent(getKey(model.getId().toString(), CacheType.ACCOUNT_RECORD_ID, model.getTableName()), model.getAccountRecordId());
         }
     }
 
-    private String getKey(final String rawKey, final CacheType cacheType, final TableName tableName) {
+    private static String getKey(final String rawKey, final CacheType cacheType, final TableName tableName) {
         return cacheType.isKeyPrefixedWithTableName() ?
                tableName + CacheControllerDispatcher.CACHE_KEY_SEPARATOR + rawKey :
                rawKey;
     }
 
-    private void updateHistoryAndAudit(final String entityId, final Map<String, M> entities, final Map<String, Long> entityRecordIds,
-                                       final ChangeType changeType, final InternalCallContext context) throws Throwable {
-
-        prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, sqlDaoClass.getSimpleName() + " (history/audit) :", new WithProfilingCallback() {
+    private M updateHistoryAndAudit(final String entityId, @Nullable final M deletedEntity, final ChangeType changeType, final InternalCallContext context) throws Throwable {
+        final Object reHydratedEntity = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, getProfilingId("history/audit", null), new WithProfilingCallback<Object, Throwable>() {
             @Override
-            public Object execute() {
-                final M reHydratedEntity = sqlDao.getById(entityId, context);
-                final Long reHydratedEntityRecordId = sqlDao.getRecordId(entityId, context);
-                final M entity = MoreObjects.firstNonNull(reHydratedEntity, entities.get(entityId));
-                final Long entityRecordId = MoreObjects.firstNonNull(reHydratedEntityRecordId, entityRecordIds.get(entityId));
-                final TableName tableName = entity.getTableName();
+            public M execute() throws Throwable {
+                final M reHydratedEntity;
+                if (changeType == ChangeType.DELETE) {
+                    reHydratedEntity = deletedEntity;
+                } else {
+                    // See note above regarding "markAsInactive" operations
+                    reHydratedEntity = MoreObjects.firstNonNull(sqlDao.getById(entityId, context), deletedEntity);
+                }
+                Preconditions.checkNotNull(reHydratedEntity, "reHydratedEntity cannot be null");
+                final Long entityRecordId = reHydratedEntity.getRecordId();
+                final TableName tableName = reHydratedEntity.getTableName();
 
                 // Note: audit entries point to the history record id
                 final Long historyRecordId;
                 if (tableName.getHistoryTableName() != null) {
-                    historyRecordId = insertHistory(entityRecordId, entity, changeType, context);
+                    historyRecordId = insertHistory(entityRecordId, reHydratedEntity, changeType, context);
                 } else {
                     historyRecordId = entityRecordId;
                 }
 
                 // Make sure to re-hydrate the object (especially needed for create calls)
-                insertAudits(tableName, entityRecordId, historyRecordId, changeType, context);
-                return null;
+                insertAudits(tableName, reHydratedEntity, entityRecordId, historyRecordId, changeType, context);
+                return reHydratedEntity;
             }
         });
+        //noinspection unchecked
+        return (M) reHydratedEntity;
     }
 
     private List<String> retrieveEntityIdsFromArguments(final Method method, final Object[] args) {
@@ -422,7 +435,7 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
                 }
             }
         }
-        return null;
+        return ImmutableList.<String>of();
     }
 
     private Builder<String> extractEntityIdsFromBatchArgument(final Iterable arg) {
@@ -453,23 +466,19 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
 
     private Long insertHistory(final Long entityRecordId, final M entityModelDao, final ChangeType changeType, final InternalCallContext context) {
         final EntityHistoryModelDao<M, E> history = new EntityHistoryModelDao<M, E>(entityModelDao, entityRecordId, changeType, clock.getUTCNow());
-
-        sqlDao.addHistoryFromTransaction(history, context);
-
-        final NonEntitySqlDao transactional = SqlObjectBuilder.attach(handle, NonEntitySqlDao.class);
-
-        /* return transactional.getLastHistoryRecordId(entityRecordId, entityModelDao.getHistoryTableName().getTableName()); */
-        return nonEntityDao.retrieveLastHistoryRecordIdFromTransaction(entityRecordId, entityModelDao.getHistoryTableName(), transactional);
+        return sqlDao.addHistoryFromTransaction(history, context);
     }
 
-    private void insertAudits(final TableName tableName, final Long entityRecordId, final Long historyRecordId, final ChangeType changeType, final InternalCallContext contextMaybeWithoutAccountRecordId) {
+    private void insertAudits(final TableName tableName, final M entityModelDao, final Long entityRecordId, final Long historyRecordId, final ChangeType changeType, final InternalCallContext contextMaybeWithoutAccountRecordId) {
         final TableName destinationTableName = MoreObjects.firstNonNull(tableName.getHistoryTableName(), tableName);
         final EntityAudit audit = new EntityAudit(destinationTableName, historyRecordId, changeType, clock.getUTCNow());
 
         final InternalCallContext context;
         // Populate the account record id when creating the account record
         if (TableName.ACCOUNT.equals(tableName) && ChangeType.INSERT.equals(changeType)) {
-            context = internalCallContextFactory.createInternalCallContext(entityRecordId, contextMaybeWithoutAccountRecordId);
+            // AccountModelDao in practice
+            final TimeZoneAwareEntity accountModelDao = (TimeZoneAwareEntity) entityModelDao;
+            context = internalCallContextFactory.createInternalCallContext(accountModelDao, entityRecordId, contextMaybeWithoutAccountRecordId);
         } else {
             context = contextMaybeWithoutAccountRecordId;
         }
@@ -478,13 +487,13 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
         // We need to invalidate the caches. There is a small window of doom here where caches will be stale.
         // TODO Knowledge on how the key is constructed is also in AuditSqlDao
         if (tableName.getHistoryTableName() != null) {
-            final CacheController<Object, Object> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG_VIA_HISTORY);
+            final CacheController<String, List> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG_VIA_HISTORY);
             if (cacheController != null) {
                 final String key = buildCacheKey(ImmutableMap.<Integer, Object>of(0, tableName.getHistoryTableName(), 1, tableName.getHistoryTableName(), 2, entityRecordId));
                 cacheController.remove(key);
             }
         } else {
-            final CacheController<Object, Object> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG);
+            final CacheController<String, List> cacheController = cacheControllerDispatcher.getCacheController(CacheType.AUDIT_LOG);
             if (cacheController != null) {
                 final String key = buildCacheKey(ImmutableMap.<Integer, Object>of(0, tableName, 1, entityRecordId));
                 cacheController.remove(key);
@@ -506,4 +515,20 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
         }
         return cacheKey.toString();
     }
+
+    private String getProfilingId(@Nullable final String prefix, @Nullable final Method method) {
+        final StringBuilder stringBuilder = new StringBuilder().append(sqlDaoClass.getSimpleName());
+
+        if (prefix != null) {
+            stringBuilder.append(" (")
+                         .append(prefix)
+                         .append(")");
+        }
+
+        if (method != null) {
+            stringBuilder.append(": ").append(method.getName());
+        }
+
+        return stringBuilder.toString();
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/TimeZoneAwareEntity.java b/util/src/main/java/org/killbill/billing/util/entity/dao/TimeZoneAwareEntity.java
new file mode 100644
index 0000000..feff107
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/TimeZoneAwareEntity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.entity.dao;
+
+import org.joda.time.DateTimeZone;
+import org.killbill.billing.util.entity.Entity;
+
+public interface TimeZoneAwareEntity extends Entity {
+
+    public DateTimeZone getTimeZone();
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
index 5a3b660..d4ddf25 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/CacheModule.java
@@ -18,14 +18,31 @@
 
 package org.killbill.billing.util.glue;
 
+import javax.cache.CacheManager;
+
 import org.killbill.billing.platform.api.KillbillConfigSource;
+import org.killbill.billing.util.cache.AccountBCDCacheLoader;
+import org.killbill.billing.util.cache.AccountRecordIdCacheLoader;
+import org.killbill.billing.util.cache.AuditLogCacheLoader;
+import org.killbill.billing.util.cache.AuditLogViaHistoryCacheLoader;
+import org.killbill.billing.util.cache.BaseCacheLoader;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.cache.CacheControllerDispatcherProvider;
-import org.killbill.billing.util.cache.EhCacheCacheManagerProvider;
+import org.killbill.billing.util.cache.ImmutableAccountCacheLoader;
+import org.killbill.billing.util.cache.ObjectIdCacheLoader;
+import org.killbill.billing.util.cache.OverriddenPlanCacheLoader;
+import org.killbill.billing.util.cache.RecordIdCacheLoader;
+import org.killbill.billing.util.cache.TenantCacheLoader;
+import org.killbill.billing.util.cache.TenantCatalogCacheLoader;
+import org.killbill.billing.util.cache.TenantConfigCacheLoader;
+import org.killbill.billing.util.cache.TenantKVCacheLoader;
+import org.killbill.billing.util.cache.TenantOverdueConfigCacheLoader;
+import org.killbill.billing.util.cache.TenantRecordIdCacheLoader;
+import org.killbill.billing.util.cache.TenantStateMachineConfigCacheLoader;
 import org.killbill.billing.util.config.definition.EhCacheConfig;
 import org.skife.config.ConfigurationObjectFactory;
 
-import net.sf.ehcache.CacheManager;
+import com.google.inject.multibindings.Multibinder;
 
 public class CacheModule extends KillBillModule {
 
@@ -39,9 +56,26 @@ public class CacheModule extends KillBillModule {
         bind(EhCacheConfig.class).toInstance(config);
 
         // EhCache specifics
-        bind(CacheManager.class).toProvider(EhCacheCacheManagerProvider.class).asEagerSingleton();
+        bind(CacheManager.class).toProvider(Eh107CacheManagerProvider.class).asEagerSingleton();
 
         // Kill Bill generic cache dispatcher
         bind(CacheControllerDispatcher.class).toProvider(CacheControllerDispatcherProvider.class).asEagerSingleton();
+
+        final Multibinder<BaseCacheLoader> resultSetMapperSetBinder = Multibinder.newSetBinder(binder(), BaseCacheLoader.class);
+        resultSetMapperSetBinder.addBinding().to(ImmutableAccountCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(AccountBCDCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(RecordIdCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(AccountRecordIdCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantRecordIdCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(ObjectIdCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(AuditLogCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(AuditLogViaHistoryCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantCatalogCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantConfigCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantOverdueConfigCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantKVCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(OverriddenPlanCacheLoader.class).asEagerSingleton();
+        resultSetMapperSetBinder.addBinding().to(TenantStateMachineConfigCacheLoader.class).asEagerSingleton();
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/glue/CacheProviderBase.java b/util/src/main/java/org/killbill/billing/util/glue/CacheProviderBase.java
new file mode 100644
index 0000000..24f2dbf
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/CacheProviderBase.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.glue;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import javax.cache.Cache;
+import javax.cache.CacheManager;
+import javax.cache.configuration.MutableConfiguration;
+
+import org.killbill.billing.util.config.definition.EhCacheConfig;
+import org.killbill.xmlloader.UriAccessor;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jcache.JCacheGaugeSet;
+
+abstract class CacheProviderBase {
+
+    private static final String PROP_METRIC_REG_JCACHE_STATISTICS = "jcache.statistics";
+
+    private final MetricRegistry metricRegistry;
+
+    final URL xmlConfigurationURL;
+
+    CacheProviderBase(final MetricRegistry metricRegistry, final EhCacheConfig cacheConfig) {
+        this.metricRegistry = metricRegistry;
+
+        try {
+            xmlConfigurationURL = UriAccessor.toURL(cacheConfig.getCacheConfigLocation());
+        } catch (final IOException e) {
+            throw new RuntimeException(e);
+        } catch (final URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    <K, V> Cache<K, V> createCache(final CacheManager cacheManager, final String cacheName, final Class<K> keyType, final Class<V> valueType) {
+        // Make sure we start from a clean state - this is mainly useful for tests
+        cacheManager.destroyCache(cacheName);
+
+        // All other configuration options come from the ehcache.xml
+        final MutableConfiguration<K, V> configuration = new MutableConfiguration<K, V>().setTypes(keyType, valueType)
+                                                                                         .setStoreByValue(false); // Store by reference to avoid copying large objects (e.g. catalog)
+        final Cache<K, V> cache = cacheManager.createCache(cacheName, configuration);
+
+        // Re-create the metrics to support dynamically created caches (e.g. for Shiro)
+        metricRegistry.removeMatching(new MetricFilter() {
+            @Override
+            public boolean matches(final String name, final Metric metric) {
+                return name != null && name.startsWith(PROP_METRIC_REG_JCACHE_STATISTICS);
+            }
+        });
+        metricRegistry.register(PROP_METRIC_REG_JCACHE_STATISTICS, new JCacheGaugeSet());
+
+        return cache;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/Eh107CacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/Eh107CacheManagerProvider.java
new file mode 100644
index 0000000..005afa3
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/Eh107CacheManagerProvider.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.glue;
+
+import java.net.URISyntaxException;
+import java.util.Set;
+
+import javax.cache.CacheManager;
+import javax.cache.Caching;
+import javax.cache.spi.CachingProvider;
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.killbill.billing.util.cache.BaseCacheLoader;
+import org.killbill.billing.util.config.definition.EhCacheConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+
+// EhCache specific provider
+public class Eh107CacheManagerProvider extends CacheProviderBase implements Provider<CacheManager> {
+
+    private static final Logger logger = LoggerFactory.getLogger(Eh107CacheManagerProvider.class);
+
+    private final Set<BaseCacheLoader> cacheLoaders;
+
+    @Inject
+    public Eh107CacheManagerProvider(final MetricRegistry metricRegistry,
+                                     final EhCacheConfig cacheConfig,
+                                     final Set<BaseCacheLoader> cacheLoaders) {
+        super(metricRegistry, cacheConfig);
+        this.cacheLoaders = cacheLoaders;
+    }
+
+    @Override
+    public CacheManager get() {
+        // JSR-107 registration, required for JMX integration
+        final CachingProvider cachingProvider = Caching.getCachingProvider();
+
+        CacheManager cacheManager;
+        try {
+            cacheManager = cachingProvider.getCacheManager(xmlConfigurationURL.toURI(), getClass().getClassLoader());
+        } catch (final RuntimeException e) {
+            logger.error("Unable to read ehcache.xml, using default configuration", e);
+            cacheManager = cachingProvider.getCacheManager();
+        } catch (final URISyntaxException e) {
+            logger.error("Unable to read ehcache.xml, using default configuration", e);
+            cacheManager = cachingProvider.getCacheManager();
+        }
+
+        for (final BaseCacheLoader<?, ?> cacheLoader : cacheLoaders) {
+            createCache(cacheManager,
+                        cacheLoader.getCacheType().getCacheName(),
+                        cacheLoader.getCacheType().getKeyType(),
+                        cacheLoader.getCacheType().getValueType());
+        }
+
+        return cacheManager;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/EhcacheShiroManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/EhcacheShiroManagerProvider.java
new file mode 100644
index 0000000..943450c
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/EhcacheShiroManagerProvider.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.glue;
+
+import java.lang.reflect.Field;
+
+import javax.cache.CacheManager;
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import org.apache.shiro.cache.Cache;
+import org.apache.shiro.cache.CacheException;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.mgt.SecurityManager;
+import org.ehcache.integrations.shiro.EhcacheShiro;
+import org.ehcache.integrations.shiro.EhcacheShiroManager;
+import org.killbill.billing.util.config.definition.EhCacheConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+
+public class EhcacheShiroManagerProvider extends CacheProviderBase implements Provider<EhcacheShiroManager> {
+
+    private final SecurityManager securityManager;
+    private final CacheManager eh107CacheManager;
+    private final org.ehcache.CacheManager ehcacheCacheManager;
+
+    @Inject
+    public EhcacheShiroManagerProvider(final SecurityManager securityManager,
+                                       final CacheManager eh107CacheManager,
+                                       final MetricRegistry metricRegistry,
+                                       final EhCacheConfig cacheConfig) {
+        super(metricRegistry, cacheConfig);
+        this.securityManager = securityManager;
+        this.eh107CacheManager = eh107CacheManager;
+        this.ehcacheCacheManager = getEhcacheManager();
+    }
+
+    @Override
+    public EhcacheShiroManager get() {
+        final EhcacheShiroManager shiroEhCacheManager = new EhcacheShiroManagerWrapper(this);
+        // Same EhCache manager instance as the rest of the system
+        shiroEhCacheManager.setCacheManager(ehcacheCacheManager);
+
+        if (securityManager instanceof DefaultSecurityManager) {
+            // For RBAC only (see also KillbillJdbcTenantRealmProvider)
+            final DefaultSecurityManager securityManager = (DefaultSecurityManager) this.securityManager;
+            securityManager.setCacheManager(shiroEhCacheManager);
+            securityManager.setSubjectDAO(new KillBillSubjectDAO());
+        }
+
+        return shiroEhCacheManager;
+    }
+
+    // Shiro isn't JCache compatible
+    private org.ehcache.CacheManager getEhcacheManager() {
+        try {
+            final Field f = eh107CacheManager.getClass().getDeclaredField("ehCacheManager");
+            f.setAccessible(true);
+
+            return (org.ehcache.CacheManager) f.get(eh107CacheManager);
+        } catch (final IllegalAccessException e) {
+            throw new RuntimeException(e);
+        } catch (final NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // Custom createCache implementation going through JCache layer to enable stats, etc.
+    private final class EhcacheShiroManagerWrapper extends EhcacheShiroManager {
+
+        private final Logger log = LoggerFactory.getLogger(EhcacheShiroManagerWrapper.class);
+
+        private final EhcacheShiroManagerProvider ehcacheShiroManagerProvider;
+
+        EhcacheShiroManagerWrapper(final EhcacheShiroManagerProvider ehcacheShiroManagerProvider) {
+            this.ehcacheShiroManagerProvider = ehcacheShiroManagerProvider;
+        }
+
+        public <K, V> Cache<K, V> getCache(final String name) throws CacheException {
+            log.trace("Acquiring EhcacheShiro instance named [{}]", name);
+
+            org.ehcache.Cache<Object, Object> cache = getCacheManager().getCache(name, Object.class, Object.class);
+
+            if (cache == null) {
+                log.info("Cache with name {} does not yet exist.  Creating now.", name);
+                ehcacheShiroManagerProvider.createCache(eh107CacheManager, name, Object.class, Object.class);
+                cache = getCacheManager().getCache(name, Object.class, Object.class);
+                log.info("Added EhcacheShiro named [{}]", name);
+            } else {
+                log.info("Using existing EhcacheShiro named [{}]", name);
+            }
+
+            return new EhcacheShiro<K, V>(cache);
+        }
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
index 4e48913..bd9ceb2 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -86,7 +86,7 @@ public class KillBillShiroModule extends ShiroModule {
         super.bindSecurityManager(bind);
 
         // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+        bind(CacheManager.class).toProvider(EhcacheShiroManagerProvider.class).asEagerSingleton();
     }
 
     @Override
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillSubjectDAO.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillSubjectDAO.java
new file mode 100644
index 0000000..176956f
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillSubjectDAO.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Groupon, Inc
+ * Copyright 2017 The Billing Project, LLC
+ *
+ * The Billing Project 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 org.killbill.billing.util.glue;
+
+import org.apache.shiro.mgt.DefaultSubjectDAO;
+import org.apache.shiro.mgt.SessionsSecurityManager;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.session.mgt.DefaultSessionManager;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.subject.support.DelegatingSubject;
+import org.apache.shiro.util.CollectionUtils;
+import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
+
+public class KillBillSubjectDAO extends DefaultSubjectDAO {
+
+    @Override
+    protected void saveToSession(final Subject subject) {
+        boolean updatesDisabled = false;
+
+        Session session = subject.getSession(false);
+        if (session == null && !CollectionUtils.isEmpty(subject.getPrincipals())) {
+            // Force the creation of the session here to get the id
+            session = subject.getSession();
+            // Optimize the session creation path: the default saveToSession implementation
+            // will call setAttribute() several times in a row, causing unnecessary DAO UPDATE queries
+            updatesDisabled = disableUpdatesForSession(subject, session);
+        }
+
+        super.saveToSession(subject);
+
+        if (updatesDisabled) {
+            enableUpdatesForSession(subject, session);
+        }
+    }
+
+    private boolean disableUpdatesForSession(final Subject subject, final Session session) {
+        final JDBCSessionDao sessionDAO = getJDBCSessionDao(subject);
+        if (sessionDAO != null) {
+            sessionDAO.disableUpdatesForSession(session);
+            return true;
+        }
+        return false;
+    }
+
+    private void enableUpdatesForSession(final Subject subject, final Session session) {
+        final JDBCSessionDao sessionDAO = getJDBCSessionDao(subject);
+        if (sessionDAO != null) {
+            sessionDAO.enableUpdatesForSession(session);
+        }
+    }
+
+    private JDBCSessionDao getJDBCSessionDao(final Subject subject) {
+        if (subject instanceof DelegatingSubject) {
+            final DelegatingSubject delegatingSubject = (DelegatingSubject) subject;
+            if (delegatingSubject.getSecurityManager() instanceof SessionsSecurityManager) {
+                final SessionsSecurityManager securityManager = (SessionsSecurityManager) delegatingSubject.getSecurityManager();
+                if (securityManager.getSessionManager() instanceof DefaultSessionManager) {
+                    final DefaultSessionManager sessionManager = (DefaultSessionManager) securityManager.getSessionManager();
+                    if (sessionManager.getSessionDAO() instanceof JDBCSessionDao) {
+                        return (JDBCSessionDao) sessionManager.getSessionDAO();
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
index 2e69423..4ff3026 100644
--- a/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
+++ b/util/src/main/java/org/killbill/billing/util/jackson/ObjectMapper.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -17,11 +19,18 @@
 package org.killbill.billing.util.jackson;
 
 import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.smile.SmileFactory;
 import com.fasterxml.jackson.datatype.joda.JodaModule;
 
 public class ObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
-    public ObjectMapper() {
+
+    public ObjectMapper(final SmileFactory f) {
+        super(f);
         this.registerModule(new JodaModule());
         this.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
     }
+
+    public ObjectMapper() {
+        this(null);
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
index 41a05eb..a6a5a34 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionDao.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -21,6 +21,7 @@ package org.killbill.billing.util.security.shiro.dao;
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 
@@ -31,12 +32,17 @@ import org.skife.jdbi.v2.IDBI;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+
 public class JDBCSessionDao extends CachingSessionDAO {
 
     private static final Logger log = LoggerFactory.getLogger(JDBCSessionDao.class);
 
     private final JDBCSessionSqlDao jdbcSessionSqlDao;
 
+    private final Cache<Serializable, Boolean> noUpdateSessionsCache = CacheBuilder.<Serializable, Boolean>newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).build();
+
     @Inject
     public JDBCSessionDao(final IDBI dbi) {
         this.jdbcSessionSqlDao = dbi.onDemand(JDBCSessionSqlDao.class);
@@ -44,7 +50,9 @@ public class JDBCSessionDao extends CachingSessionDAO {
 
     @Override
     protected void doUpdate(final Session session) {
-        jdbcSessionSqlDao.update(new SessionModelDao(session));
+        if (shouldUpdateSession(session)) {
+            jdbcSessionSqlDao.update(new SessionModelDao(session));
+        }
     }
 
     @Override
@@ -56,9 +64,12 @@ public class JDBCSessionDao extends CachingSessionDAO {
     protected Serializable doCreate(final Session session) {
         final UUID sessionId = UUIDs.randomUUID();
         // See SessionModelDao#toSimpleSession for why we use toString()
-        assignSessionId(session, sessionId.toString());
+        final String sessionIdAsString = sessionId.toString();
+        assignSessionId(session, sessionIdAsString);
         jdbcSessionSqlDao.create(new SessionModelDao(session));
-        return sessionId;
+        // Make sure to return a String here as well, or Shiro will cache the Session with a UUID key
+        // while it is expecting String
+        return sessionIdAsString;
     }
 
     @Override
@@ -82,4 +93,17 @@ public class JDBCSessionDao extends CachingSessionDAO {
             return null;
         }
     }
+
+    public void disableUpdatesForSession(final Session session) {
+        noUpdateSessionsCache.put(session.getId(), Boolean.TRUE);
+    }
+
+    public void enableUpdatesForSession(final Session session) {
+        noUpdateSessionsCache.invalidate(session.getId());
+        doUpdate(session);
+    }
+
+    private boolean shouldUpdateSession(final Session session) {
+        return noUpdateSessionsCache.getIfPresent(session.getId()) == Boolean.TRUE ? Boolean.FALSE : Boolean.TRUE;
+    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
index 6c78854..90d2d75 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/dao/DefaultTagDefinitionDao.java
@@ -155,7 +155,7 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
 
                     // Create it
                     final TagDefinitionModelDao tagDefinition = new TagDefinitionModelDao(context.getCreatedDate(), definitionName, description);
-                    tagDefinitionSqlDao.create(tagDefinition, context);
+                    createAndRefresh(tagDefinitionSqlDao, tagDefinition, context);
 
                     // Post an event to the bus
                     final boolean isControlTag = TagModelDaoHelper.isControlTag(tagDefinition.getName());
diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml
index 61d098d..b2d732c 100644
--- a/util/src/main/resources/ehcache.xml
+++ b/util/src/main/resources/ehcache.xml
@@ -2,8 +2,8 @@
 
 <!--
   ~ Copyright 2010-2014 Ning, Inc.
-  ~ Copyright 2014-2016 Groupon, Inc
-  ~ Copyright 2014-2016 The Billing Project, LLC
+  ~ Copyright 2014-2017 Groupon, Inc
+  ~ Copyright 2014-2017 The Billing Project, LLC
   ~
   ~ The Billing Project 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
@@ -18,221 +18,42 @@
   ~ under the License.
   -->
 
-<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:noNamespaceSchemaLocation="ehcache.xsd">
-
-    <defaultCache
-            maxElementsInMemory="100000"
-            maxElementsOnDisk="0"
-            eternal="true"
-            overflowToDisk="false"
-            diskPersistent="false"
-            memoryStoreEvictionPolicy="LFU"
-            statistics="true"
-            />
-
-    <cache name="record-id"
-           maxElementsInMemory="100000"
-           maxElementsOnDisk="0"
-           eternal="true"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-record-id"
-           maxElementsInMemory="100000"
-           maxElementsOnDisk="0"
-           eternal="true"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="account-record-id"
-           maxElementsInMemory="100000"
-           maxElementsOnDisk="0"
-           eternal="true"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="object-id"
-           maxElementsInMemory="100000"
-           maxElementsOnDisk="0"
-           eternal="true"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-
-    <cache name="audit-log"
-           maxElementsInMemory="500000"
-           maxElementsOnDisk="0"
-           timeToIdleSeconds="600"
-           timeToLiveSeconds="600"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="audit-log-via-history"
-           maxElementsInMemory="500000"
-           maxElementsOnDisk="0"
-           timeToIdleSeconds="600"
-           timeToLiveSeconds="600"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-catalog"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-overdue-config"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true">
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-config"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true">
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-kv"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="overridden-plan"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="account-immutable"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="account-bcd"
-           maxElementsInMemory="1000"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-
-    <cache name="tenant"
-           maxElementsInMemory="100"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-    <cache name="tenant-payment-state-machine-config"
-           maxElementsInMemory="100"
-           maxElementsOnDisk="0"
-           overflowToDisk="false"
-           diskPersistent="false"
-           memoryStoreEvictionPolicy="LFU"
-           statistics="true"
-            >
-        <cacheEventListenerFactory
-                class="org.killbill.billing.util.cache.ExpirationListenerFactory"
-                properties=""/>
-    </cache>
-
-</ehcache>
+<ehcache:config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
+                xmlns:ehcache='http://www.ehcache.org/v3'
+                xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
+                xmlns:terracotta='http://www.ehcache.org/v3/clustered'
+                xsi:schemaLocation="http://www.ehcache.org/v3
+                                    http://www.ehcache.org/schema/ehcache-core-3.3.xsd
+                                    http://www.ehcache.org/v3/jsr107
+                                    http://www.ehcache.org/schema/ehcache-107-ext-3.3.xsd
+                                    http://www.ehcache.org/v3/clustered
+                                    http://www.ehcache.org/schema/ehcache-clustered-ext-3.3.xsd">
+    <ehcache:service>
+        <jsr107:defaults default-template="defaultCacheConfiguration" enable-management="true" enable-statistics="true">
+            <!-- See AuditSqlDao -->
+            <jsr107:cache name="audit-log" template="defaultShortTTLCacheConfiguration"/>
+            <jsr107:cache name="audit-log-via-history" template="defaultShortTTLCacheConfiguration"/>
+        </jsr107:defaults>
+    </ehcache:service>
+
+    <ehcache:cache-template name="defaultShortTTLCacheConfiguration">
+        <ehcache:expiry>
+            <ehcache:ttl unit="seconds">20</ehcache:ttl>
+        </ehcache:expiry>
+
+        <ehcache:resources>
+            <ehcache:heap unit="entries">100000</ehcache:heap>
+        </ehcache:resources>
+    </ehcache:cache-template>
+
+    <ehcache:cache-template name="defaultCacheConfiguration">
+        <ehcache:expiry>
+            <ehcache:none/>
+        </ehcache:expiry>
+
+        <ehcache:resources>
+            <ehcache:heap unit="entries">100000</ehcache:heap>
+        </ehcache:resources>
+    </ehcache:cache-template>
+</ehcache:config>
 
diff --git a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
index a79883d..2d96592 100644
--- a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -336,6 +336,18 @@ auditTableValues() ::= <<
 <if(tenantRecordIdField(""))>, <tenantRecordIdValue()><endif>
 >>
 
+getHistoryForTargetRecordId() ::= <<
+select
+  <idField()>
+, <historyTableFields("t.")>
+<accountRecordIdFieldWithComma("t.")>
+<tenantRecordIdFieldWithComma("t.")>
+from <historyTableName()> t
+where <targetRecordIdField("t.")> = :targetRecordId
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+;
+>>
 
 addHistoryFromTransaction() ::= <<
 insert into <historyTableName()> (
diff --git a/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
index 5893704..0a1812e 100644
--- a/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
+++ b/util/src/test/java/org/killbill/billing/dao/MockNonEntityDao.java
@@ -46,42 +46,42 @@ public class MockNonEntityDao implements NonEntityDao {
     }
 
     @Override
-    public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return null;
     }
 
     @Override
-    public Long retrieveRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         return null;
     }
 
     @Override
-    public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveAccountRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return accountRecordIdMappings.get(objectId);
     }
 
     @Override
-    public Long retrieveAccountRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveAccountRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         return null;
     }
 
     @Override
-    public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public Long retrieveTenantRecordIdFromObject(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache) {
         return tenantRecordIdMappings.get(objectId);
     }
 
     @Override
-    public Long retrieveTenantRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public Long retrieveTenantRecordIdFromObjectInTransaction(final UUID objectId, final ObjectType objectType, @Nullable final CacheController<String, Long> cache, @Nullable final Handle handle) {
         return null;
     }
 
     @Override
-    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache) {
+    public UUID retrieveIdFromObject(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache) {
         return null;
     }
 
     @Override
-    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<Object, Object> cache, @Nullable final Handle handle) {
+    public UUID retrieveIdFromObjectInTransaction(final Long recordId, final ObjectType objectType, @Nullable final CacheController<String, UUID> cache, @Nullable final Handle handle) {
         return null;
     }
 
diff --git a/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java b/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java
index 9a5c01e..40e940e 100644
--- a/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java
+++ b/util/src/test/java/org/killbill/billing/util/callcontext/TestInternalCallContextFactory.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2012 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -19,6 +21,9 @@ package org.killbill.billing.util.callcontext;
 import java.util.Date;
 import java.util.UUID;
 
+import org.killbill.billing.account.api.ImmutableAccountData;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.mockito.Mockito;
 import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.tweak.HandleCallback;
 import org.testng.Assert;
@@ -36,6 +41,9 @@ public class TestInternalCallContextFactory extends UtilTestSuiteWithEmbeddedDB 
         final UUID invoiceId = UUID.randomUUID();
         final Long accountRecordId = 19384012L;
 
+        final ImmutableAccountData immutableAccountData = Mockito.mock(ImmutableAccountData.class);
+        Mockito.when(immutableAccountInternalApi.getImmutableAccountDataByRecordId(Mockito.<Long>eq(accountRecordId), Mockito.<InternalTenantContext>any())).thenReturn(immutableAccountData);
+
         dbi.withHandle(new HandleCallback<Void>() {
             @Override
             public Void withHandle(final Handle handle) throws Exception {
@@ -71,6 +79,9 @@ public class TestInternalCallContextFactory extends UtilTestSuiteWithEmbeddedDB 
         final UUID accountId = UUID.randomUUID();
         final Long accountRecordId = 19384012L;
 
+        final ImmutableAccountData immutableAccountData = Mockito.mock(ImmutableAccountData.class);
+        Mockito.when(immutableAccountInternalApi.getImmutableAccountDataByRecordId(Mockito.<Long>eq(accountRecordId), Mockito.<InternalTenantContext>any())).thenReturn(immutableAccountData);
+
         dbi.withHandle(new HandleCallback<Void>() {
             @Override
             public Void withHandle(final Handle handle) throws Exception {
diff --git a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
index 2dccd4c..ffe76e3 100644
--- a/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
+++ b/util/src/test/java/org/killbill/billing/util/customfield/api/TestDefaultCustomFieldUserApi.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -24,11 +24,14 @@ import java.util.Map;
 import java.util.UUID;
 
 import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
 import org.killbill.billing.util.customfield.CustomField;
 import org.killbill.billing.util.customfield.StringCustomField;
 import org.killbill.billing.util.entity.Pagination;
+import org.mockito.Mockito;
 import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.tweak.HandleCallback;
 import org.testng.Assert;
@@ -43,6 +46,9 @@ public class TestDefaultCustomFieldUserApi extends UtilTestSuiteWithEmbeddedDB {
         final UUID accountId = UUID.randomUUID();
         final Long accountRecordId = 19384012L;
 
+        final ImmutableAccountData immutableAccountData = Mockito.mock(ImmutableAccountData.class);
+        Mockito.when(immutableAccountInternalApi.getImmutableAccountDataByRecordId(Mockito.<Long>eq(accountRecordId), Mockito.<InternalTenantContext>any())).thenReturn(immutableAccountData);
+
         dbi.withHandle(new HandleCallback<Void>() {
             @Override
             public Void withHandle(final Handle handle) throws Exception {
diff --git a/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java b/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
index 566518d..c4a8c52 100644
--- a/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
+++ b/util/src/test/java/org/killbill/billing/util/security/TestPermissionAnnotationMethodInterceptor.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project 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:
  *
@@ -20,6 +22,13 @@ import javax.inject.Singleton;
 
 import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.authz.UnauthenticatedException;
+import org.killbill.billing.security.Permission;
+import org.killbill.billing.security.RequiresPermissions;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.billing.util.glue.CacheModule;
+import org.killbill.billing.util.glue.KillBillShiroAopModule;
 import org.killbill.billing.util.glue.TestSecurityModuleNoDB;
 import org.killbill.billing.util.glue.TestUtilModuleNoDB.ShiroModuleNoDB;
 import org.mockito.Mockito;
@@ -27,18 +36,10 @@ import org.skife.jdbi.v2.IDBI;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import org.killbill.billing.security.Permission;
-import org.killbill.billing.security.RequiresPermissions;
-import org.killbill.billing.util.UtilTestSuiteNoDB;
-import org.killbill.billing.util.glue.KillBillShiroAopModule;
-import org.killbill.billing.util.glue.KillBillShiroModule;
-import org.killbill.billing.util.glue.SecurityModule;
-
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Stage;
-import net.sf.ehcache.CacheManager;
 
 public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB {
 
@@ -73,16 +74,18 @@ public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB
 
         // Now, verify the interception works
         configureShiro();
-        // Shutdown the cache manager to avoid duplicate exceptions
-        CacheManager.getInstance().shutdown();
+
         final Injector injector = Guice.createInjector(Stage.PRODUCTION,
                                                        new ShiroModuleNoDB(configSource),
                                                        new KillBillShiroAopModule(),
                                                        new TestSecurityModuleNoDB(configSource),
+                                                       new CacheModule(configSource),
                                                        new AbstractModule() {
                                                            @Override
                                                            protected void configure() {
                                                                bind(IDBI.class).toInstance(Mockito.mock(IDBI.class));
+                                                               bind(TenantInternalApi.class).toInstance(Mockito.mock(TenantInternalApi.class));
+                                                               bind(NonEntityDao.class).toInstance(Mockito.mock(NonEntityDao.class));
                                                            }
                                                        });
         final AopTester aopedTester = injector.getInstance(AopTester.class);
@@ -101,17 +104,19 @@ public class TestPermissionAnnotationMethodInterceptor extends UtilTestSuiteNoDB
 
         // Now, verify the interception works
         configureShiro();
-        // Shutdown the cache manager to avoid duplicate exceptions
-        CacheManager.getInstance().shutdown();
+
         final Injector injector = Guice.createInjector(Stage.PRODUCTION,
                                                        new ShiroModuleNoDB(configSource),
                                                        new KillBillShiroAopModule(),
                                                        new TestSecurityModuleNoDB(configSource),
+                                                       new CacheModule(configSource),
                                                        new AbstractModule() {
                                                            @Override
                                                            public void configure() {
                                                                bind(IDBI.class).toInstance(Mockito.mock(IDBI.class));
                                                                bind(IAopTester.class).to(AopTesterImpl.class).asEagerSingleton();
+                                                               bind(TenantInternalApi.class).toInstance(Mockito.mock(TenantInternalApi.class));
+                                                               bind(NonEntityDao.class).toInstance(Mockito.mock(NonEntityDao.class));
                                                            }
                                                        });
         final IAopTester aopedTester = injector.getInstance(IAopTester.class);
diff --git a/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java b/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java
index 978f5bb..bbdfb0f 100644
--- a/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java
+++ b/util/src/test/java/org/killbill/billing/util/tag/api/TestDefaultTagUserApi.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -23,11 +23,14 @@ import java.util.Map;
 import java.util.UUID;
 
 import org.killbill.billing.ObjectType;
+import org.killbill.billing.account.api.ImmutableAccountData;
 import org.killbill.billing.api.TestApiListener.NextEvent;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.UtilTestSuiteWithEmbeddedDB;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.tag.ControlTagType;
 import org.killbill.billing.util.tag.Tag;
+import org.mockito.Mockito;
 import org.skife.jdbi.v2.Handle;
 import org.skife.jdbi.v2.tweak.HandleCallback;
 import org.testng.Assert;
@@ -42,6 +45,9 @@ public class TestDefaultTagUserApi extends UtilTestSuiteWithEmbeddedDB {
         final UUID accountId = UUID.randomUUID();
         final Long accountRecordId = 19384012L;
 
+        final ImmutableAccountData immutableAccountData = Mockito.mock(ImmutableAccountData.class);
+        Mockito.when(immutableAccountInternalApi.getImmutableAccountDataByRecordId(Mockito.<Long>eq(accountRecordId), Mockito.<InternalTenantContext>any())).thenReturn(immutableAccountData);
+
         dbi.withHandle(new HandleCallback<Void>() {
             @Override
             public Void withHandle(final Handle handle) throws Exception {
diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
index 17ce189..503a229 100644
--- a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
+++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2014 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -21,6 +21,7 @@ package org.killbill.billing.util;
 import javax.inject.Inject;
 
 import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
+import org.killbill.billing.account.api.ImmutableAccountInternalApi;
 import org.killbill.billing.api.TestApiListener;
 import org.killbill.billing.security.api.SecurityApi;
 import org.killbill.billing.util.audit.dao.AuditDao;
@@ -66,6 +67,8 @@ public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
     @Inject
     protected InternalCallContextFactory internalCallContextFactory;
     @Inject
+    protected ImmutableAccountInternalApi immutableAccountInternalApi;
+    @Inject
     protected DefaultTagUserApi tagUserApi;
     @Inject
     protected DefaultCustomFieldUserApi customFieldUserApi;