killbill-uncached

cache: make all cached objects Serializable This is required

3/27/2017 8:18:13 AM

Details

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..03885a9 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,40 @@
 
 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.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;
     }
 
@@ -68,7 +80,7 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
 
     @Override
     public DateTimeZone getTimeZone() {
-        return dateTimeZone;
+        return timeZone;
     }
 
     @Override
@@ -86,7 +98,7 @@ public class DefaultImmutableAccountData implements ImmutableAccountData {
     }
 
     public DateTimeZone getFixedOffsetTimeZone() {
-        return fixedOffsetDateTimeZone;
+        return fixedOffsetTimeZone;
     }
 
     @Override
@@ -100,8 +112,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 +139,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 +153,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/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();
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);
+    }
 }
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/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java b/payment/src/main/java/org/killbill/billing/payment/caching/EhCacheStateMachineConfigCache.java
index 2d1c3dc..19194c6 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
@@ -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");
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/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..58c87e7 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 VerySimpleByteSource(bytes));
 
         return authenticationInfo;
     }
@@ -78,4 +89,56 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
     private void configureDataSource() {
         setDataSource(dataSource);
     }
+
+    private static final class VerySimpleByteSource implements ByteSource, Externalizable {
+
+        private static final long serialVersionUID = 4498655519894503985L;
+
+        private byte[] bytes;
+        private String cachedHex;
+        private String cachedBase64;
+
+        // For deserialization
+        public VerySimpleByteSource() {}
+
+        VerySimpleByteSource(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/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);
+    }
 }

util/pom.xml 4(+4 -0)

diff --git a/util/pom.xml b/util/pom.xml
index 1a4974b..3769272 100644
--- a/util/pom.xml
+++ b/util/pom.xml
@@ -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>
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/Cachable.java b/util/src/main/java/org/killbill/billing/util/cache/Cachable.java
index eb90621..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
@@ -53,6 +53,7 @@ public @interface Cachable {
 
     CacheType value();
 
+    // Make sure both the key and value are Serializable
     enum CacheType {
 
         /* Mapping from object 'id (UUID as String)' -> object 'recordId (Long)' */
@@ -67,19 +68,19 @@ public @interface Cachable {
         /* 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>' */
+        /* 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>' */
+        /* 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, Long.class, Catalog.class, false),
 
-        /* Tenant payment state machine config cache */
+        /* 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 cache (String -> DefaultOverdueConfig) */
         TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, Long.class, Object.class, false),
 
         /* Tenant overdue config cache */
@@ -88,7 +89,7 @@ public @interface Cachable {
         /* Tenant config cache */
         TENANT_KV(TENANT_KV_CACHE_NAME, String.class, String.class, false),
 
-        /* Tenant config cache */
+        /* Tenant cache */
         TENANT(TENANT_CACHE_NAME, String.class, Tenant.class, false),
 
         /* Overwritten plans  */
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/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/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/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);
+    }
 }