killbill-memoizeit

Changes

pom.xml 2(+1 -1)

Details

diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index b42b21f..5679190 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -28,6 +28,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
@@ -40,6 +41,9 @@ public interface SubscriptionBaseInternalApi {
     public SubscriptionBase createSubscription(UUID bundleId, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, DateTime requestedDateWithMs,
                                                InternalCallContext context) throws SubscriptionBaseApiException;
 
+    public SubscriptionBase createBaseSubscriptionWithAddOns(UUID bundleId, Iterable<EntitlementSpecifier> entitlements, DateTime requestedDateWithMs,
+                                                             InternalCallContext context) throws SubscriptionBaseApiException;
+
     public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
             throws SubscriptionBaseApiException;
 
diff --git a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
index cf29529..3a39d4f 100644
--- a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
@@ -22,7 +22,6 @@ import java.util.Locale;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.tenant.api.TenantKV.TenantKey;
-import org.killbill.billing.util.callcontext.TenantContext;
 
 public interface TenantInternalApi {
 
@@ -53,4 +52,7 @@ public interface TenantInternalApi {
     public String getPluginConfig(String pluginName, InternalTenantContext tenantContext);
 
     public List<String> getTenantValuesForKey(final String key, final InternalTenantContext tenantContext);
+
+    public Tenant getTenantByApiKey(final String key) throws TenantApiException;
+
 }
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 0d21887..c1cfa81 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
@@ -23,25 +23,30 @@ import java.util.List;
 
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
-import org.killbill.billing.entitlement.api.SubscriptionEventType;
-import org.killbill.billing.invoice.api.DryRunType;
-import org.testng.annotations.Test;
-
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.api.TestApiListener.NextEvent;
 import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.DefaultEntitlementSpecifier;
+import org.killbill.billing.entitlement.api.Entitlement;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
+import org.killbill.billing.entitlement.api.SubscriptionEventType;
+import org.killbill.billing.invoice.api.DryRunType;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.InvoiceItemType;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
 
 public class TestSubscription extends TestIntegrationBase {
 
@@ -175,4 +180,61 @@ public class TestSubscription extends TestIntegrationBase {
 
         checkNoMoreInvoiceToGenerate(account);
     }
+
+    @Test(groups = "slow")
+    public void testCreateSubscriptionWithAddOns() throws Exception {
+        final LocalDate initialDate = new LocalDate(2015, 10, 1);
+        clock.setDay(initialDate);
+
+        final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final String externalKey = "baseExternalKey";
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier(baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier1 = new DefaultEntitlementSpecifier(addOnSpec1, null);
+        EntitlementSpecifier addOnEntitlementSpecifier2 = new DefaultEntitlementSpecifier(addOnSpec2, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier1);
+        specifierList.add(addOnEntitlementSpecifier2);
+
+        busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.CREATE, NextEvent.CREATE, NextEvent.INVOICE, NextEvent.PAYMENT);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+        checkNoMoreInvoiceToGenerate(account);
+
+        assertNotNull(entitlement);
+
+        final List<Entitlement> allEntitlementsForBundle = entitlementApi.getAllEntitlementsForBundle(entitlement.getBundleId(), callContext);
+        assertTrue(allEntitlementsForBundle.size() == 3);
+
+        final Entitlement baseEntitlement = allEntitlementsForBundle.get(0);
+        final Entitlement addOnEntitlement1 = allEntitlementsForBundle.get(1);
+        final Entitlement addOnEntitlement2 = allEntitlementsForBundle.get(2);
+
+        assertEquals(baseEntitlement.getLastActiveProduct().getName(), "Shotgun");
+        assertEquals(baseEntitlement.getLastActiveProductCategory(), ProductCategory.BASE);
+
+        assertEquals(addOnEntitlement1.getLastActiveProduct().getName(), "Telescopic-Scope");
+        assertEquals(addOnEntitlement1.getLastActiveProductCategory(), ProductCategory.ADD_ON);
+
+        assertEquals(addOnEntitlement2.getLastActiveProduct().getName(), "Laser-Scope");
+        assertEquals(addOnEntitlement2.getLastActiveProductCategory(), ProductCategory.ADD_ON);
+
+        final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), callContext);
+        assertTrue(invoices.size() == 1); // ONLY ONE INVOICE
+        assertTrue(invoices.get(0).getInvoiceItems().size() == 3);
+
+        final ImmutableList<ExpectedInvoiceItemCheck> toBeChecked = ImmutableList.<ExpectedInvoiceItemCheck>of(
+                new ExpectedInvoiceItemCheck(initialDate, new LocalDate(2015, 10, 31), InvoiceItemType.RECURRING, new BigDecimal("387.05")), // amount=387.05, rate=399.95 -> Telescopic-Scope
+                new ExpectedInvoiceItemCheck(initialDate, new LocalDate(2015, 10, 31), InvoiceItemType.RECURRING, new BigDecimal("967.69")), // amount=967.69, rate=999.95 -> Laser-Scope
+                new ExpectedInvoiceItemCheck(initialDate, null, InvoiceItemType.FIXED, new BigDecimal("0"))); // Shotgun
+
+        invoiceChecker.checkInvoice(invoices.get(0).getId(), callContext, toBeChecked);
+
+    }
 }
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
index 1b7ccbc..bc8a935 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithEntilementPlugin.java
@@ -34,6 +34,8 @@ import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.DefaultEntitlement;
+import org.killbill.billing.entitlement.api.DefaultEntitlementSpecifier;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
 import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApi;
 import org.killbill.billing.entitlement.plugin.api.EntitlementPluginApiException;
@@ -138,6 +140,10 @@ public class TestWithEntilementPlugin extends TestIntegrationBase {
         @Override
         public PriorEntitlementResult priorCall(final EntitlementContext entitlementContext, final Iterable<PluginProperty> properties) throws EntitlementPluginApiException {
             if (planPhasePriceOverride != null) {
+                final EntitlementSpecifier entitlementSpecifier = new DefaultEntitlementSpecifier(entitlementContext.getEntitlementSpecifiers().get(0).getPlanPhaseSpecifier(), planPhasePriceOverride);
+                final List<EntitlementSpecifier> entitlementSpecifiers = new ArrayList<EntitlementSpecifier>();
+                entitlementSpecifiers.add(entitlementSpecifier);
+
                 return new PriorEntitlementResult() {
                     @Override
                     public boolean isAborted() {
@@ -145,18 +151,13 @@ public class TestWithEntilementPlugin extends TestIntegrationBase {
                     }
 
                     @Override
-                    public PlanPhaseSpecifier getAdjustedPlanPhaseSpecifier() {
-                        return null;
-                    }
-
-                    @Override
                     public LocalDate getAdjustedEffectiveDate() {
                         return null;
                     }
 
                     @Override
-                    public List<PlanPhasePriceOverride> getAdjustedPlanPhasePriceOverride() {
-                        return planPhasePriceOverride;
+                    public List<EntitlementSpecifier> getAdjustedEntitlementSpecifiers() {
+                        return entitlementSpecifiers;
                     }
 
                     @Override
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePhaseDefinitionSqlDao.java b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePhaseDefinitionSqlDao.java
index 2e70525..87061c9 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePhaseDefinitionSqlDao.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePhaseDefinitionSqlDao.java
@@ -22,15 +22,15 @@ import java.util.List;
 
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface CatalogOverridePhaseDefinitionSqlDao extends Transactional<CatalogOverridePhaseDefinitionSqlDao>, CloseMe {
 
     @SqlUpdate
@@ -52,8 +52,6 @@ public interface CatalogOverridePhaseDefinitionSqlDao extends Transactional<Cata
     public List<CatalogOverridePhaseDefinitionModelDao> getOverriddenPlanPhases(@Bind("targetPlanDefRecordId") Long targetPlanDefRecordId,
                                                                                 @SmartBindBean final InternalTenantContext context);
 
-
     @SqlQuery
     public Long getLastInsertId();
-
 }
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanDefinitionSqlDao.java b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanDefinitionSqlDao.java
index 6c73970..8570039 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanDefinitionSqlDao.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanDefinitionSqlDao.java
@@ -19,18 +19,15 @@ package org.killbill.billing.catalog.dao;
 
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
-import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
 import org.skife.jdbi.v2.sqlobject.Bind;
-import org.skife.jdbi.v2.sqlobject.BindBean;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
-import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
 import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface CatalogOverridePlanDefinitionSqlDao extends Transactional<CatalogOverridePlanDefinitionSqlDao>, CloseMe {
 
     @SqlUpdate
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanPhaseSqlDao.java b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanPhaseSqlDao.java
index e4f0e55..3993ddf 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanPhaseSqlDao.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/dao/CatalogOverridePlanPhaseSqlDao.java
@@ -18,20 +18,18 @@
 package org.killbill.billing.catalog.dao;
 
 import java.util.Collection;
-import java.util.List;
 
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.util.tag.dao.UUIDCollectionBinder;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface CatalogOverridePlanPhaseSqlDao extends Transactional<CatalogOverridePlanPhaseSqlDao>, CloseMe {
 
     @SqlUpdate
@@ -49,5 +47,4 @@ public interface CatalogOverridePlanPhaseSqlDao extends Transactional<CatalogOve
 
     @SqlQuery
     public Long getLastInsertId();
-
 }
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 59d7909..9ff10b3 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -21,7 +21,6 @@ import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
-import org.apache.shiro.SecurityUtils;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.joda.time.LocalDate;
@@ -372,7 +371,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                                                                                getAccountId(),
                                                                                null,
                                                                                getBundleId(),
-                                                                               null,
                                                                                getExternalKey(),
                                                                                null,
                                                                                localCancelDate,
@@ -439,7 +437,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                                                                                getAccountId(),
                                                                                null,
                                                                                getBundleId(),
-                                                                               null,
                                                                                getExternalKey(),
                                                                                null,
                                                                                null,
@@ -488,7 +485,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                                                                                getAccountId(),
                                                                                null,
                                                                                getBundleId(),
-                                                                               null,
                                                                                getExternalKey(),
                                                                                null,
                                                                                localDate,
@@ -537,7 +533,6 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
                                                                                getAccountId(),
                                                                                null,
                                                                                getBundleId(),
-                                                                               null,
                                                                                getExternalKey(),
                                                                                null,
                                                                                localDate,
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 8d5f729..e8983c2 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
@@ -16,6 +16,7 @@
 
 package org.killbill.billing.entitlement.api;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
@@ -31,6 +32,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.AccountEventsStreams;
 import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.EventsStream;
@@ -117,13 +119,15 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
     @Override
     public Entitlement createBaseEntitlement(final UUID accountId, final PlanPhaseSpecifier planPhaseSpecifier, final String externalKey, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
+        EntitlementSpecifier entitlementSpecifier = new DefaultEntitlementSpecifier(planPhaseSpecifier, overrides);
+        final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+        entitlementSpecifierList.add(entitlementSpecifier);
         final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
                                                                                accountId,
                                                                                null,
                                                                                null,
-                                                                               planPhaseSpecifier,
                                                                                externalKey,
-                                                                               overrides,
+                                                                               entitlementSpecifierList,
                                                                                effectiveDate,
                                                                                properties,
                                                                                callContext);
@@ -142,7 +146,8 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
 
                     final DateTime referenceTime = clock.getUTCNow();
                     final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getEffectiveDate(), referenceTime, contextWithValidAccountRecordId);
-                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundle.getId(), updatedPluginContext.getPlanPhaseSpecifier(), updatedPluginContext.getPlanPhasePriceOverride(), requestedDate, contextWithValidAccountRecordId);
+                    final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(updatedPluginContext.getEntitlementSpecifiers());
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundle.getId(), specifier.getPlanPhaseSpecifier(), specifier.getOverrides(), requestedDate, contextWithValidAccountRecordId);
 
                     return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
                                                   blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
@@ -155,16 +160,84 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
         return pluginExecution.executeWithPlugin(createBaseEntitlementWithPlugin, pluginContext);
     }
 
+    private EntitlementSpecifier getFirstEntitlementSpecifier(final List<EntitlementSpecifier> entitlementSpecifiers) throws SubscriptionBaseApiException {
+        if ((entitlementSpecifiers == null) || entitlementSpecifiers.isEmpty()) {
+            throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
+        }
+        return entitlementSpecifiers.get(0);
+    }
+
+    @Override
+    public Entitlement createBaseEntitlementWithAddOns(final UUID accountId, final String externalKey, final Iterable<EntitlementSpecifier> entitlementSpecifiers,
+                                                       final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext)
+            throws EntitlementApiException {
+
+        final EntitlementSpecifier baseSpecifier = Iterables.tryFind(entitlementSpecifiers, new Predicate<EntitlementSpecifier>() {
+            @Override
+            public boolean apply(final EntitlementSpecifier specifier) {
+                return specifier.getPlanPhaseSpecifier() != null && ProductCategory.BASE.equals(specifier.getPlanPhaseSpecifier().getProductCategory());
+            }
+        }).orNull();
+
+        if (baseSpecifier == null) {
+            throw new EntitlementApiException(new IllegalArgumentException(), ErrorCode.SUB_CREATE_NO_BP.getCode(), "Missing Base Subscription.");
+        }
+
+        final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+        Iterables.addAll(entitlementSpecifierList, entitlementSpecifiers);
+
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTIONS_WITH_AO,
+                                                                               accountId,
+                                                                               null,
+                                                                               null,
+                                                                               externalKey,
+                                                                               entitlementSpecifierList,
+                                                                               effectiveDate,
+                                                                               properties,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Entitlement> createBaseEntitlementWithAddOn = new WithEntitlementPlugin<Entitlement>() {
+            @Override
+            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);
+
+                    final DateTime referenceTime = clock.getUTCNow();
+                    final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getEffectiveDate(), referenceTime, contextWithValidAccountRecordId);
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createBaseSubscriptionWithAddOns(bundle.getId(), entitlementSpecifiers, requestedDate, contextWithValidAccountRecordId);
+
+                    return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
+                                                  blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
+                                                  entitlementUtils, dateHelper, clock, securityApi, internalCallContextFactory, callContext);
+
+
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+
+            }
+        };
+        return pluginExecution.executeWithPlugin(createBaseEntitlementWithAddOn, pluginContext);
+    }
+
     @Override
     public Entitlement addEntitlement(final UUID bundleId, final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
+        EntitlementSpecifier entitlementSpecifier = new DefaultEntitlementSpecifier(planPhaseSpecifier, overrides);
+        final List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+        entitlementSpecifierList.add(entitlementSpecifier);
         final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
                                                                                null,
                                                                                null,
                                                                                bundleId,
-                                                                               planPhaseSpecifier,
                                                                                null,
-                                                                               overrides,
+                                                                               entitlementSpecifierList,
                                                                                effectiveDate,
                                                                                properties,
                                                                                callContext);
@@ -188,7 +261,8 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
 
                 try {
                     final InternalCallContext context = internalCallContextFactory.createInternalCallContext(callContext);
-                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundleId, updatedPluginContext.getPlanPhaseSpecifier(), updatedPluginContext.getPlanPhasePriceOverride(), requestedDate, context);
+                    final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(updatedPluginContext.getEntitlementSpecifiers());
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundleId, specifier.getPlanPhaseSpecifier(), specifier.getOverrides(), requestedDate, context);
 
                     return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
                                                   blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
@@ -313,9 +387,8 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
                                                                                sourceAccountId,
                                                                                destAccountId,
                                                                                null,
-                                                                               null,
                                                                                externalKey,
-                                                                               null,
+                                                                               new ArrayList<EntitlementSpecifier>(),
                                                                                effectiveDate,
                                                                                properties,
                                                                                context);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
index 47ada7b..79da30c 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
@@ -24,8 +24,6 @@ import javax.annotation.Nullable;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
-import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
-import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
 import org.killbill.billing.entitlement.plugin.api.OperationType;
 import org.killbill.billing.entitlement.plugin.api.PriorEntitlementResult;
@@ -41,9 +39,8 @@ public class DefaultEntitlementContext implements EntitlementContext {
     private final UUID accountId;
     private final UUID destinationAccountId;
     private final UUID bundleId;
-    private final PlanPhaseSpecifier spec;
     private final String externalKey;
-    private final List<PlanPhasePriceOverride> planPhasePriceOverrides;
+    private final List<EntitlementSpecifier> entitlementSpecifiers;
     private final LocalDate effectiveDate;
     private final Iterable<PluginProperty> pluginProperties;
     private final UUID userToken;
@@ -63,9 +60,8 @@ public class DefaultEntitlementContext implements EntitlementContext {
              prev.getAccountId(),
              prev.getDestinationAccountId(),
              prev.getBundleId(),
-             pluginResult != null && pluginResult.getAdjustedPlanPhaseSpecifier() != null ? pluginResult.getAdjustedPlanPhaseSpecifier() : prev.getPlanPhaseSpecifier(),
              prev.getExternalKey(),
-             pluginResult != null && pluginResult.getAdjustedPlanPhasePriceOverride() != null ? pluginResult.getAdjustedPlanPhasePriceOverride() : prev.getPlanPhasePriceOverride(),
+             pluginResult != null && pluginResult.getAdjustedEntitlementSpecifiers() != null ? pluginResult.getAdjustedEntitlementSpecifiers() : prev.getEntitlementSpecifiers(),
              pluginResult != null && pluginResult.getAdjustedEffectiveDate() != null ? pluginResult.getAdjustedEffectiveDate() : prev.getEffectiveDate(),
              pluginResult != null && pluginResult.getAdjustedPluginProperties() != null ? pluginResult.getAdjustedPluginProperties() : prev.getPluginProperties(),
              prev);
@@ -75,13 +71,12 @@ public class DefaultEntitlementContext implements EntitlementContext {
                                      final UUID accountId,
                                      final UUID destinationAccountId,
                                      final UUID bundleId,
-                                     final PlanPhaseSpecifier spec,
                                      final String externalKey,
-                                     final List<PlanPhasePriceOverride> planPhasePriceOverrides,
+                                     final List<EntitlementSpecifier> entitlementSpecifiers,
                                      final LocalDate effectiveDate,
                                      final Iterable<PluginProperty> pluginProperties,
                                      final CallContext callContext) {
-        this(operationType, accountId, destinationAccountId, bundleId, spec, externalKey, planPhasePriceOverrides, effectiveDate, pluginProperties,
+        this(operationType, accountId, destinationAccountId, bundleId, externalKey, entitlementSpecifiers, effectiveDate, pluginProperties,
              callContext.getUserToken(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(),
              callContext.getComments(), callContext.getCreatedDate(), callContext.getUpdatedDate(), callContext.getTenantId());
     }
@@ -91,9 +86,8 @@ public class DefaultEntitlementContext implements EntitlementContext {
                                      final UUID accountId,
                                      final UUID destinationAccountId,
                                      final UUID bundleId,
-                                     final PlanPhaseSpecifier spec,
                                      final String externalKey,
-                                     final List<PlanPhasePriceOverride> planPhasePriceOverrides,
+                                     final List<EntitlementSpecifier> entitlementSpecifiers,
                                      final LocalDate effectiveDate,
                                      final Iterable<PluginProperty> pluginProperties,
                                      final UUID userToken,
@@ -109,9 +103,8 @@ public class DefaultEntitlementContext implements EntitlementContext {
         this.accountId = accountId;
         this.destinationAccountId = destinationAccountId;
         this.bundleId = bundleId;
-        this.spec = spec;
         this.externalKey = externalKey;
-        this.planPhasePriceOverrides = planPhasePriceOverrides;
+        this.entitlementSpecifiers = entitlementSpecifiers;
         this.effectiveDate = effectiveDate;
         this.pluginProperties = pluginProperties;
         this.userToken = userToken;
@@ -146,18 +139,13 @@ public class DefaultEntitlementContext implements EntitlementContext {
     }
 
     @Override
-    public PlanPhaseSpecifier getPlanPhaseSpecifier() {
-        return spec;
-    }
-
-    @Override
     public String getExternalKey() {
         return externalKey;
     }
 
     @Override
-    public List<PlanPhasePriceOverride> getPlanPhasePriceOverride() {
-        return planPhasePriceOverrides;
+    public List<EntitlementSpecifier> getEntitlementSpecifiers() {
+        return entitlementSpecifiers;
     }
 
     @Override
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
new file mode 100644
index 0000000..dbf7876
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.entitlement.api;
+
+import java.util.List;
+
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+
+public class DefaultEntitlementSpecifier implements EntitlementSpecifier {
+
+    private final PlanPhaseSpecifier planPhaseSpecifier;
+    private final List<PlanPhasePriceOverride> overrides;
+
+    public DefaultEntitlementSpecifier(final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides) {
+        this.planPhaseSpecifier = planPhaseSpecifier;
+        this.overrides = overrides;
+    }
+
+    @Override
+    public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+        return planPhaseSpecifier;
+    }
+
+    @Override
+    public List<PlanPhasePriceOverride> getOverrides() {
+        return overrides;
+    }
+
+}
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
index e092b73..9651c95 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/svcs/DefaultEntitlementApiBase.java
@@ -157,7 +157,6 @@ public class DefaultEntitlementApiBase {
                                                                                bundleId,
                                                                                null,
                                                                                null,
-                                                                               null,
                                                                                localEffectiveDate,
                                                                                properties,
                                                                                internalCallContextFactory.createCallContext(internalCallContext));
@@ -216,7 +215,6 @@ public class DefaultEntitlementApiBase {
                                                                                bundleId,
                                                                                null,
                                                                                null,
-                                                                               null,
                                                                                localEffectiveDate,
                                                                                properties,
                                                                                internalCallContextFactory.createCallContext(internalCallContext));
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 8d87964..9a397ae 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
@@ -16,6 +16,7 @@
 
 package org.killbill.billing.entitlement.api;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
@@ -41,8 +42,10 @@ import org.testng.annotations.Test;
 import com.google.common.collect.ImmutableList;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedDB {
 
@@ -558,4 +561,102 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
 
     }
 
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithAddOns() throws AccountApiException, EntitlementApiException, SubscriptionBaseApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Cleaning", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final String externalKey = "baseExternalKey";
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier(baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier = new DefaultEntitlementSpecifier(addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier);
+
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.CREATE);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertNotNull(entitlement);
+
+        final List<Entitlement> allEntitlementsForBundle = entitlementApi.getAllEntitlementsForBundle(entitlement.getBundleId(), callContext);
+        assertTrue(allEntitlementsForBundle.size() == 2);
+
+        final Entitlement baseEntitlement = allEntitlementsForBundle.get(0);
+        final Entitlement addOnEntitlement = allEntitlementsForBundle.get(1);
+
+        assertEquals(baseEntitlement.getLastActiveProduct().getName(), "Pistol");
+        assertEquals(baseEntitlement.getLastActiveProductCategory(), ProductCategory.BASE);
+
+        assertEquals(addOnEntitlement.getLastActiveProduct().getName(), "Cleaning");
+        assertEquals(addOnEntitlement.getLastActiveProductCategory(), ProductCategory.ADD_ON);
+
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithInvalidAddOn() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Invalid", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final String externalKey = "baseExternalKey";
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier(baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier = new DefaultEntitlementSpecifier(addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier);
+
+        try {
+            entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+            fail();
+        } catch (EntitlementApiException e) {
+            assertEquals(e.getMessage(), "Could not find any product named 'Invalid'");
+        }
+
+        final List<Entitlement> allEntitlementsForAccount = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        assertTrue(allEntitlementsForAccount.size() == 0);
+
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithoutBaseEntitlement() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Cleaning", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Bullets", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        final String externalKey = "addOnExternalKey";
+        EntitlementSpecifier addOnEntitlementSpecifier1 = new DefaultEntitlementSpecifier(baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier2 = new DefaultEntitlementSpecifier(addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(addOnEntitlementSpecifier1);
+        specifierList.add(addOnEntitlementSpecifier2);
+
+        try {
+            entitlementApi.createBaseEntitlementWithAddOns(account.getId(), externalKey, specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+            fail();
+        } catch (EntitlementApiException e) {
+            assertEquals(e.getMessage(), "Missing Base Subscription.");
+        }
+
+        final List<Entitlement> allEntitlementsForAccount = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        assertTrue(allEntitlementsForAccount.size() == 0);
+
+    }
+
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 28cd1aa..6292294 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -470,4 +470,9 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
             Preconditions.checkArgument(expression, errorMessage);
         }
     }
+
+    protected void verifyNumberOfElements(int actual, int expected, String errorMessage) {
+        Preconditions.checkArgument(actual == expected, errorMessage);
+    }
+
 }
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 a692dd6..dc2af68 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
@@ -19,6 +19,7 @@
 package org.killbill.billing.jaxrs.resources;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeoutException;
@@ -54,6 +55,7 @@ import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.entitlement.api.EntitlementApi;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.entitlement.api.SubscriptionApi;
 import org.killbill.billing.entitlement.api.SubscriptionApiException;
@@ -87,6 +89,10 @@ import org.killbill.commons.metrics.TimedResource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -160,17 +166,16 @@ public class SubscriptionResource extends JaxRsResourceBase {
                              entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set",
                              entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set",
                              entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
-
-        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        // For ADD_ON we can provide externalKey or the bundleId
         final boolean createAddOnEntitlement = ProductCategory.ADD_ON.toString().equals(entitlement.getProductCategory());
         if (createAddOnEntitlement) {
-            verifyNonNullOrEmpty(entitlement.getBundleId(), "SubscriptionJson bundleId should be specified for ADD_ON");
-        } else {
-            verifyNonNullOrEmpty(entitlement.getAccountId(), "SubscriptionJson accountId should be specified for BP");
+            Preconditions.checkArgument(entitlement.getExternalKey() != null || entitlement.getBundleId() != null, "SubscriptionJson bundleId or externalKey should be specified for ADD_ON");
         }
 
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
         final CallContext callContext = context.createContext(createdBy, reason, comment, request);
 
+
         final EntitlementCallCompletionCallback<Entitlement> callback = new EntitlementCallCompletionCallback<Entitlement>() {
             @Override
             public Entitlement doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException, SubscriptionApiException, AccountApiException {
@@ -181,17 +186,26 @@ public class SubscriptionResource extends JaxRsResourceBase {
                                                                        BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
 
                 final LocalDate inputLocalDate = toLocalDate(account, requestedDate, callContext);
-                final UUID bundleId = entitlement.getBundleId() != null ? UUID.fromString(entitlement.getBundleId()) : null;
                 final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
                                                                  ProductCategory.valueOf(entitlement.getProductCategory()),
                                                                  BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
 
                 final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
                 return createAddOnEntitlement ?
-                       entitlementApi.addEntitlement(bundleId, spec, overrides, inputLocalDate, pluginProperties, callContext) :
+                       entitlementApi.addEntitlement(getBundleIdForAddOnCreation(entitlement), spec, overrides, inputLocalDate, pluginProperties, callContext) :
                        entitlementApi.createBaseEntitlement(account.getId(), spec, entitlement.getExternalKey(), overrides, inputLocalDate, pluginProperties, callContext);
             }
 
+            private UUID getBundleIdForAddOnCreation(final SubscriptionJson entitlement) throws SubscriptionApiException {
+
+                if (entitlement.getBundleId() != null) {
+                    return UUID.fromString(entitlement.getBundleId());
+                }
+                // If user only specified the externalKey we need to fech the bundle (expensive operation) to extract the bundleId
+                final SubscriptionBundle bundle = subscriptionApi.getActiveSubscriptionBundleForExternalKey(entitlement.getExternalKey(), callContext);
+                return bundle.getId();
+            }
+
             @Override
             public boolean isImmOperation() {
                 return true;
@@ -207,7 +221,117 @@ public class SubscriptionResource extends JaxRsResourceBase {
         return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
     }
 
+    @TimedResource
+    @POST
+    @Path("/createEntitlementWithAddOns")
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Create an entitlement with addOn products")
+    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid entitlement supplied")})
+    public Response createEntitlementWithAddOns(final List<SubscriptionJson> entitlements,
+                                      @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+                                      @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
+                                      @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
+                                      @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                      @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                      @HeaderParam(HDR_REASON) final String reason,
+                                      @HeaderParam(HDR_COMMENT) final String comment,
+                                      @javax.ws.rs.core.Context final 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.");
+
+        for (SubscriptionJson entitlement : entitlements) {
+            verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified for each element");
+            verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set for each element",
+                                 entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set for each element",
+                                 entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set for each element",
+                                 entitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
+        }
+
+        final int baseSubscriptionsSize = Iterables.size(Iterables.filter(entitlements, new Predicate<SubscriptionJson>() {
+            @Override
+            public boolean apply(final SubscriptionJson subscription) {
+                return subscription.getProductCategory().equals(ProductCategory.BASE.toString());
+            }
+        }));
+        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 subscription.getProductCategory().equals(ProductCategory.ADD_ON.toString());
+            }
+        }));
+        verifyNumberOfElements(addOnSubscriptionsSize, entitlements.size() - 1, "It should be " + (entitlements.size() - 1) + " ADD_ON products.");
+
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        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 CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        final EntitlementCallCompletionCallback<Entitlement> callback = new EntitlementCallCompletionCallback<Entitlement>() {
+
+            @Override
+            public Entitlement doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException, SubscriptionApiException, AccountApiException {
+
+                List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+                final Account account = getAccountFromSubscriptionJson(baseEntitlement, callContext);
+
+                for (final SubscriptionJson entitlement : entitlements) {
+
+                    final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(entitlement.getProductName(),
+                                                                                         ProductCategory.valueOf(entitlement.getProductCategory()),
+                                                                                         BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
+
+                    final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
+                                                                    ProductCategory.valueOf(entitlement.getProductCategory()),
+                                                                    BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
+                    final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
+
+                    EntitlementSpecifier specifier = new EntitlementSpecifier() {
+
+                        @Override
+                        public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+                            return planPhaseSpecifier;
+                        }
+
+                        @Override
+                        public List<PlanPhasePriceOverride> getOverrides() {
+                            return overrides;
+                        }
+                    };
+
+                    entitlementSpecifierList.add(specifier);
+                }
+
+                final LocalDate inputLocalDate = toLocalDate(account, requestedDate, callContext);
+                return entitlementApi.createBaseEntitlementWithAddOns(account.getId(), baseEntitlement.getExternalKey(), entitlementSpecifierList,
+                                                                      inputLocalDate, pluginProperties, callContext);
+            }
+
+            @Override
+            public boolean isImmOperation() {
+                return true;
+            }
+
+            @Override
+            public Response doResponseOk(final Entitlement entitlement) {
+                return uriBuilder.buildResponse(uriInfo, BundleResource.class, "getBundle", entitlement.getBundleId());
+            }
+
+        };
+
+        final EntitlementCallCompletion<Entitlement> callCompletionCreation = new EntitlementCallCompletion<Entitlement>();
+        return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+    }
 
     @TimedResource
     @PUT

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 09b97b7..4d1d1da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.62</version>
+        <version>0.66</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.15.10-SNAPSHOT</version>
diff --git a/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
new file mode 100644
index 0000000..14c5eda
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticatorWith540.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015 Groupon, Inc
+ * Copyright 2015 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.apache.shiro.authc.pam;
+
+import java.util.Collection;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.realm.Realm;
+import org.killbill.billing.server.security.FirstSuccessfulStrategyWith540;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// See https://issues.apache.org/jira/browse/SHIRO-540
+public class ModularRealmAuthenticatorWith540 extends ModularRealmAuthenticator {
+
+    private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
+
+    public ModularRealmAuthenticatorWith540(final ModularRealmAuthenticator delegate) {
+        setRealms(delegate.getRealms());
+        setAuthenticationStrategy(delegate.getAuthenticationStrategy());
+    }
+
+    /**
+     * Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object
+     * as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}.
+     *
+     * @param realms the multiple realms configured on this Authenticator instance.
+     * @param token  the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
+     * @return an aggregated AuthenticationInfo instance representing account data across all the successfully
+     * consulted realms.
+     */
+    protected AuthenticationInfo doMultiRealmAuthentication(final Collection<Realm> realms, final AuthenticationToken token) {
+
+        final AuthenticationStrategy strategy = getAuthenticationStrategy();
+
+        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
+
+        if (log.isTraceEnabled()) {
+            log.trace("Iterating through {} realms for PAM authentication", realms.size());
+        }
+
+        for (final Realm realm : realms) {
+
+            aggregate = strategy.beforeAttempt(realm, token, aggregate);
+
+            if (realm.supports(token)) {
+
+                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
+
+                AuthenticationInfo info = null;
+                Throwable t = null;
+                try {
+                    info = realm.getAuthenticationInfo(token);
+                } catch (final Throwable throwable) {
+                    t = throwable;
+                    if (log.isDebugEnabled()) {
+                        final String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
+                        log.debug(msg, t);
+                    }
+                }
+
+                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
+
+                if (strategy instanceof FirstSuccessfulStrategyWith540) {
+                    // check if we should check the next realm, or just stop here.
+                    if (!((FirstSuccessfulStrategyWith540) strategy).continueAfterAttempt(info, aggregate, t)) {
+                        log.trace("Will not consult any other realms for authentication, last realm [{}].", realm);
+                        break;
+                    }
+                }
+
+            } else {
+                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
+            }
+        }
+
+        aggregate = strategy.afterAllAttempts(token, aggregate);
+
+        return aggregate;
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java b/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java
new file mode 100644
index 0000000..f877740
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/apache/shiro/guice/web/ShiroWebModuleWith435.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.shiro.guice.web;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.AnnotatedBindingBuilder;
+import com.google.inject.name.Names;
+import com.google.inject.servlet.ServletModule;
+import org.apache.shiro.guice.ShiroModule;
+import org.apache.shiro.config.ConfigurationException;
+import org.apache.shiro.env.Environment;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.session.mgt.SessionManager;
+import org.apache.shiro.web.env.WebEnvironment;
+import org.apache.shiro.web.filter.PathMatchingFilter;
+import org.apache.shiro.web.filter.authc.*;
+import org.apache.shiro.web.filter.authz.*;
+import org.apache.shiro.web.filter.mgt.FilterChainResolver;
+import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
+import org.apache.shiro.web.mgt.WebSecurityManager;
+import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
+
+import javax.servlet.Filter;
+import javax.servlet.ServletContext;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Sets up Shiro lifecycles within Guice, enables the injecting of Shiro objects, and binds a default
+ * {@link org.apache.shiro.web.mgt.WebSecurityManager}, {@link org.apache.shiro.mgt.SecurityManager} and {@link org.apache.shiro.session.mgt.SessionManager}.  At least one realm must be added by
+ * using {@link #bindRealm() bindRealm}.
+ * <p/>
+ * Also provides for the configuring of filter chains and binds a {@link org.apache.shiro.web.filter.mgt.FilterChainResolver} with that information.
+ * @see <a href="https://issues.apache.org/jira/browse/SHIRO-435">https://issues.apache.org/jira/browse/SHIRO-435</a>
+ */
+public abstract class ShiroWebModuleWith435 extends ShiroModule {
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<AnonymousFilter> ANON = Key.get(AnonymousFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<FormAuthenticationFilter> AUTHC = Key.get(FormAuthenticationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<BasicHttpAuthenticationFilter> AUTHC_BASIC = Key.get(BasicHttpAuthenticationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<NoSessionCreationFilter> NO_SESSION_CREATION = Key.get(NoSessionCreationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<LogoutFilter> LOGOUT = Key.get(LogoutFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<PermissionsAuthorizationFilter> PERMS = Key.get(PermissionsAuthorizationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<PortFilter> PORT = Key.get(PortFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<HttpMethodPermissionFilter> REST = Key.get(HttpMethodPermissionFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<RolesAuthorizationFilter> ROLES = Key.get(RolesAuthorizationFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static final Key<UserFilter> USER = Key.get(UserFilter.class);
+
+
+    static final String NAME = "SHIRO";
+
+    /**
+     * We use a LinkedHashMap here to ensure that iterator order is the same as add order.  This is important, as the
+     * FilterChainResolver uses iterator order when searching for a matching chain.
+     */
+    private final Map<String, Key<? extends Filter>[]> filterChains = new LinkedHashMap<String, Key<? extends Filter>[]>();
+    private final ServletContext servletContext;
+
+    public ShiroWebModuleWith435(ServletContext servletContext) {
+        this.servletContext = servletContext;
+    }
+
+    public static void bindGuiceFilter(Binder binder) {
+        binder.install(guiceFilterModule());
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static void bindGuiceFilter(final String pattern, Binder binder) {
+        binder.install(guiceFilterModule(pattern));
+    }
+
+    public static ServletModule guiceFilterModule() {
+        return guiceFilterModule("/*");
+    }
+
+    public static ServletModule guiceFilterModule(final String pattern) {
+        return new ServletModule() {
+            @Override
+            protected void configureServlets() {
+                filter(pattern).through(GuiceShiroFilter.class);
+            }
+        };
+    }
+
+    @Override
+    protected final void configureShiro() {
+        bindBeanType(TypeLiteral.get(ServletContext.class), Key.get(ServletContext.class, Names.named(NAME)));
+        bind(Key.get(ServletContext.class, Names.named(NAME))).toInstance(this.servletContext);
+        bindWebSecurityManager(bind(WebSecurityManager.class));
+        bindWebEnvironment(bind(WebEnvironment.class));
+        bind(GuiceShiroFilter.class).asEagerSingleton();
+        expose(GuiceShiroFilter.class);
+
+        this.configureShiroWeb();
+
+        setupFilterChainConfigs();
+
+        bind(FilterChainResolver.class).toProvider(new FilterChainResolverProvider(filterChains));
+    }
+
+    private void setupFilterChainConfigs() {
+        Table<Key<? extends PathMatchingFilter>, String, String> configs = HashBasedTable.create();
+
+        for (Map.Entry<String, Key<? extends Filter>[]> filterChain : filterChains.entrySet()) {
+            for (int i = 0; i < filterChain.getValue().length; i++) {
+                Key<? extends Filter> key = filterChain.getValue()[i];
+                if (key instanceof FilterConfigKey) {
+                    FilterConfigKey<? extends PathMatchingFilter> configKey = (FilterConfigKey<? extends PathMatchingFilter>) key;
+                    key = configKey.getKey();
+                    filterChain.getValue()[i] = key;
+                    if (!PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
+                        throw new ConfigurationException("Config information requires a PathMatchingFilter - can't apply to " + key.getTypeLiteral().getRawType());
+                    }
+                    configs.put(castToPathMatching(key), filterChain.getKey(), configKey.getConfigValue());
+                } else if (PathMatchingFilter.class.isAssignableFrom(key.getTypeLiteral().getRawType())) {
+                    configs.put(castToPathMatching(key), filterChain.getKey(), "");
+                }
+            }
+        }
+        for (Key<? extends PathMatchingFilter> filterKey : configs.rowKeySet()) {
+            bindPathMatchingFilter(filterKey, configs.row(filterKey));
+        }
+    }
+
+    private <T extends PathMatchingFilter> void bindPathMatchingFilter(Key<T> filterKey, Map<String, String> configs) {
+        bind(filterKey).toProvider(new PathMatchingFilterProvider<T>(filterKey, configs)).asEagerSingleton();
+    }
+
+    @SuppressWarnings({"unchecked"})
+    private Key<? extends PathMatchingFilter> castToPathMatching(Key<? extends Filter> key) {
+        return (Key<? extends PathMatchingFilter>) key;
+    }
+
+    protected abstract void configureShiroWeb();
+
+    @SuppressWarnings({"unchecked"})
+    @Override
+    protected final void bindSecurityManager(AnnotatedBindingBuilder<? super SecurityManager> bind) {
+        bind.to(WebSecurityManager.class); // SHIRO-435
+    }
+
+    /**
+     * Binds the security manager.  Override this method in order to provide your own security manager binding.
+     * <p/>
+     * By default, a {@link org.apache.shiro.web.mgt.DefaultWebSecurityManager} is bound as an eager singleton.
+     *
+     * @param bind
+     */
+    protected void bindWebSecurityManager(AnnotatedBindingBuilder<? super WebSecurityManager> bind) {
+        try {
+            bind.toConstructor(DefaultWebSecurityManager.class.getConstructor(Collection.class)).asEagerSingleton();
+        } catch (NoSuchMethodException e) {
+            throw new ConfigurationException("This really shouldn't happen.  Either something has changed in Shiro, or there's a bug in ShiroModule.", e);
+        }
+    }
+
+    /**
+     * Binds the session manager.  Override this method in order to provide your own session manager binding.
+     * <p/>
+     * By default, a {@link org.apache.shiro.web.session.mgt.DefaultWebSessionManager} is bound as an eager singleton.
+     *
+     * @param bind
+     */
+    @Override
+    protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
+        bind.to(ServletContainerSessionManager.class).asEagerSingleton();
+    }
+
+    @Override
+    protected final void bindEnvironment(AnnotatedBindingBuilder<Environment> bind) {
+        bind.to(WebEnvironment.class); // SHIRO-435
+    }
+
+    protected void bindWebEnvironment(AnnotatedBindingBuilder<? super WebEnvironment> bind) {
+        bind.to(WebGuiceEnvironment.class).asEagerSingleton();
+    }
+
+    /**
+     * Adds a filter chain to the shiro configuration.
+     * <p/>
+     * NOTE: If the provided key is for a subclass of {@link org.apache.shiro.web.filter.PathMatchingFilter}, it will be registered with a proper
+     * provider.
+     *
+     * @param pattern
+     * @param keys
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected final void addFilterChain(String pattern, Key<? extends Filter>... keys) {
+        filterChains.put(pattern, keys);
+    }
+
+    protected static <T extends PathMatchingFilter> Key<T> config(Key<T> baseKey, String configValue) {
+        return new FilterConfigKey<T>(baseKey, configValue);
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected static <T extends PathMatchingFilter> Key<T> config(TypeLiteral<T> typeLiteral, String configValue) {
+        return config(Key.get(typeLiteral), configValue);
+    }
+
+    @SuppressWarnings({"UnusedDeclaration"})
+    protected static <T extends PathMatchingFilter> Key<T> config(Class<T> type, String configValue) {
+        return config(Key.get(type), configValue);
+    }
+
+    private static class FilterConfigKey<T extends PathMatchingFilter> extends Key<T> {
+        private Key<T> key;
+        private String configValue;
+
+        private FilterConfigKey(Key<T> key, String configValue) {
+            super();
+            this.key = key;
+            this.configValue = configValue;
+        }
+
+        public Key<T> getKey() {
+            return key;
+        }
+
+        public String getConfigValue() {
+            return configValue;
+        }
+    }
+}
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
new file mode 100644
index 0000000..2fb4840
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillbillJdbcTenantRealmProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.modules;
+
+import javax.inject.Named;
+import javax.sql.DataSource;
+
+import org.apache.shiro.cache.CacheManager;
+import org.killbill.billing.server.security.KillbillJdbcTenantRealm;
+import org.killbill.billing.util.config.SecurityConfig;
+import org.killbill.billing.util.glue.ShiroEhCacheInstrumentor;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class KillbillJdbcTenantRealmProvider implements Provider<KillbillJdbcTenantRealm> {
+
+    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) {
+        this.securityConfig = securityConfig;
+        this.cacheManager = cacheManager;
+        this.ehCacheInstrumentor = ehCacheInstrumentor;
+        this.dataSource = dataSource;
+    }
+
+    @Override
+    public KillbillJdbcTenantRealm get() {
+        final KillbillJdbcTenantRealm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig);
+
+        // Set the cache manager
+        // Note: the DefaultWebSecurityManager used for RBAC will have all of its realms (set in KillBillShiroWebModule)
+        // 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 0617b12..721c7a7 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
@@ -23,30 +23,47 @@ import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 
+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.ShiroWebModule;
+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.KillbillJdbcTenantRealm;
 import org.killbill.billing.util.config.RbacConfig;
 import org.killbill.billing.util.glue.EhCacheManagerProvider;
 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;
+import com.google.inject.matcher.Matchers;
+import com.google.inject.spi.InjectionListener;
+import com.google.inject.spi.TypeEncounter;
+import com.google.inject.spi.TypeListener;
 
 // For Kill Bill server only.
 // See org.killbill.billing.util.glue.KillBillShiroModule for Kill Bill library.
-public class KillBillShiroWebModule extends ShiroWebModule {
+public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
 
     private final ConfigSource configSource;
 
@@ -56,26 +73,52 @@ public class KillBillShiroWebModule extends ShiroWebModule {
     }
 
     @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();
+
+        configureShiroForRBAC();
+
+        configureShiroForTenants();
+    }
+
+    private void configureShiroForRBAC() {
         final RbacConfig config = new ConfigurationObjectFactory(configSource).build(RbacConfig.class);
         bind(RbacConfig.class).toInstance(config);
 
+        // Note: order matters (the first successful match will win, see below)
         bindRealm().toProvider(IniRealmProvider.class).asEagerSingleton();
-
         bindRealm().to(KillBillJdbcRealm.class).asEagerSingleton();
-
         if (KillBillShiroModule.isLDAPEnabled()) {
             bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
         }
 
-        // Magic provider to configure the cache manager
-        bind(CacheManager.class).toProvider(EhCacheManagerProvider.class).asEagerSingleton();
+        bindListener(new AbstractMatcher<TypeLiteral<?>>() {
+                         @Override
+                         public boolean matches(final TypeLiteral<?> o) {
+                             return Matchers.subclassesOf(WebSecurityManager.class).matches(o.getRawType());
+                         }
+                     },
+                     new DefaultWebSecurityManagerTypeListener(getProvider(ShiroEhCacheInstrumentor.class)));
 
         if (KillBillShiroModule.isRBACEnabled()) {
             addFilterChain(JaxrsResource.PREFIX + "/**", Key.get(CorsBasicHttpAuthenticationFilter.class));
         }
     }
 
+    private void configureShiroForTenants() {
+        // Realm binding for the tenants (see TenantFilter)
+        bind(KillbillJdbcTenantRealm.class).toProvider(KillbillJdbcTenantRealmProvider.class).asEagerSingleton();
+        expose(KillbillJdbcTenantRealm.class);
+    }
+
     @Override
     protected void bindSessionManager(final AnnotatedBindingBuilder<SessionManager> bind) {
         // Bypass the servlet container completely for session management and delegate it to Shiro.
@@ -97,4 +140,36 @@ public class KillBillShiroWebModule extends ShiroWebModule {
             return "OPTIONS".equalsIgnoreCase(httpMethod) || super.isAccessAllowed(request, response, mappedValue);
         }
     }
+
+    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/FirstSuccessfulStrategyWith540.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java
new file mode 100644
index 0000000..cd39ac8
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/FirstSuccessfulStrategyWith540.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 Groupon, Inc
+ * Copyright 2015 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.authc.AuthenticationInfo;
+import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
+
+public class FirstSuccessfulStrategyWith540 extends FirstSuccessfulStrategy {
+
+    public boolean continueAfterAttempt(final AuthenticationInfo singleRealmInfo, final AuthenticationInfo aggregateInfo, final Throwable t) {
+        return !(aggregateInfo != null && aggregateInfo == singleRealmInfo);
+    }
+}
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 85b17d9..20b2d7a 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
@@ -46,6 +46,10 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
         this.dataSource = dataSource;
         this.securityConfig = securityConfig;
 
+        // Note: we don't support updating tenants credentials via API
+        // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+        setAuthenticationCachingEnabled(true);
+
         configureSecurity();
         configureQueries();
         configureDataSource();
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
index 82f63d1..a0af60a 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -21,7 +21,6 @@ package org.killbill.billing.server.security;
 import java.io.IOException;
 
 import javax.inject.Inject;
-import javax.inject.Named;
 import javax.inject.Singleton;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -31,7 +30,6 @@ import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
 
 import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.AuthenticationToken;
@@ -40,11 +38,9 @@ import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
 import org.apache.shiro.realm.Realm;
 import org.killbill.billing.jaxrs.resources.JaxrsResource;
 import org.killbill.billing.server.listeners.KillbillGuiceListener;
-import org.killbill.billing.server.modules.KillbillPlatformModule;
 import org.killbill.billing.tenant.api.Tenant;
 import org.killbill.billing.tenant.api.TenantApiException;
 import org.killbill.billing.tenant.api.TenantUserApi;
-import org.killbill.billing.util.config.SecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,17 +57,12 @@ public class TenantFilter implements Filter {
     @Inject
     protected TenantUserApi tenantUserApi;
     @Inject
-    protected SecurityConfig securityConfig;
-
-    @Inject
-    @Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED)
-    protected DataSource dataSource;
+    protected KillbillJdbcTenantRealm killbillJdbcTenantRealm;
 
     private ModularRealmAuthenticator modularRealmAuthenticator;
 
     @Override
     public void init(final FilterConfig filterConfig) throws ServletException {
-        final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig);
         // We use Shiro to verify the api credentials - but the Shiro Subject is only used for RBAC
         modularRealmAuthenticator = new ModularRealmAuthenticator();
         modularRealmAuthenticator.setRealms(ImmutableList.<Realm>of(killbillJdbcTenantRealm));
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 33252d8..2f5ca6d 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
@@ -31,9 +31,9 @@ import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.ProductCategory;
-import org.killbill.billing.catalog.override.PriceOverride;
 import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.model.Account;
+import org.killbill.billing.client.model.Bundle;
 import org.killbill.billing.client.model.Invoice;
 import org.killbill.billing.client.model.PhasePriceOverride;
 import org.killbill.billing.client.model.Subscription;
@@ -228,4 +228,49 @@ public class TestEntitlement extends TestJaxrsBase {
         assertEquals(invoices.get(0).getAmount().compareTo(BigDecimal.TEN), 0);
     }
 
+    @Test(groups = "slow", description = "Create a base entitlement and also addOns entitlements under the same bundle")
+    public void testEntitlementWithAddOns() 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 base = new Subscription();
+        base.setAccountId(accountJson.getAccountId());
+        base.setExternalKey("base");
+        base.setProductName("Shotgun");
+        base.setProductCategory(ProductCategory.BASE);
+        base.setBillingPeriod(BillingPeriod.MONTHLY);
+        base.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
+
+        final Subscription addOn1 = new Subscription();
+        addOn1.setAccountId(accountJson.getAccountId());
+        addOn1.setExternalKey("");
+        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.setExternalKey("");
+        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 Bundle bundle = killBillClient.createSubscriptionWithAddOns(subscriptions, initialDate, 10, "createdBy", "", "");
+        assertNotNull(bundle);
+        assertEquals(bundle.getExternalKey(), "base");
+        assertEquals(bundle.getSubscriptions().size(), 3);
+
+        final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, AuditLevel.FULL);
+        assertEquals(invoices.size(), 1);
+    }
+
 }
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 fa48420..5044598 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
@@ -22,6 +22,7 @@ import java.util.EventListener;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -196,6 +197,9 @@ public class TestJaxrsBase extends KillbillClient {
         clock.resetDeltaFromReality();
         clock.setDay(new LocalDate(2012, 8, 25));
 
+        // Make sure to re-generate the api key and secret (could be cached by Shiro)
+        DEFAULT_API_KEY = UUID.randomUUID().toString();
+        DEFAULT_API_SECRET = UUID.randomUUID().toString();
         loginTenant(DEFAULT_API_KEY, DEFAULT_API_SECRET);
 
         // Recreate the tenant (tables have been cleaned-up)
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index a96cac4..26c6a3d 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -35,6 +35,7 @@ import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.TenantContext;
@@ -46,6 +47,9 @@ public interface SubscriptionBaseApiService {
                                               CallContext context)
             throws SubscriptionBaseApiException;
 
+    public DefaultSubscriptionBase createPlans(Iterable<SubscriptionSpecifier> subscriptions, CallContext context)
+            throws SubscriptionBaseApiException;
+
     @Deprecated
     public boolean recreatePlan(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, DateTime requestedDateWithMs, CallContext context)
             throws SubscriptionBaseApiException;
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 33a57d3..5155064 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
@@ -46,6 +46,7 @@ import org.killbill.billing.catalog.api.ProductCategory;
 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.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.subscription.api.SubscriptionApiBase;
@@ -62,6 +63,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.SubscriptionSpecifier;
 import org.killbill.billing.subscription.engine.addon.AddonUtils;
 import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
 import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
@@ -162,6 +164,59 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
+    public SubscriptionBase createBaseSubscriptionWithAddOns(final UUID bundleId, final Iterable<EntitlementSpecifier> entitlements, final DateTime requestedDateWithMs, final InternalCallContext context) throws SubscriptionBaseApiException {
+
+        final DateTime now = clock.getUTCNow();
+        final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+
+        try {
+            final List<SubscriptionSpecifier> subscriptions = new ArrayList<SubscriptionSpecifier>();
+            final Catalog catalog = catalogService.getFullCatalog(context);
+            final CallContext callContext = internalCallContextFactory.createCallContext(context);
+
+            for (EntitlementSpecifier entitlement : entitlements) {
+
+                final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+                final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
+
+                final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
+
+                final Plan plan = catalog.createOrFindPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, overridesWithContext, requestedDate);
+                final PlanPhase phase = plan.getAllPhases()[0];
+                if (phase == null) {
+                    throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
+                                                                  spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+                }
+
+                final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, context);
+                if (bundle == null) {
+                    throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleId);
+                }
+
+                SubscriptionSpecifier subscription = new SubscriptionSpecifier();
+                subscription.setRealPriceList(realPriceList);
+                subscription.setRequestedDate(requestedDate);
+                subscription.setEffectiveDate(requestedDate);
+                subscription.setProcessedDate(now);
+                subscription.setPlan(plan);
+                subscription.setInitialPhase(spec.getPhaseType());
+                subscription.setBuilder(new SubscriptionBuilder()
+                                                .setId(UUIDs.randomUUID())
+                                                .setBundleId(bundleId)
+                                                .setCategory(plan.getProduct().getCategory())
+                                                .setBundleStartDate(requestedDate)
+                                                .setAlignStartDate(requestedDate));
+
+                subscriptions.add(subscription);
+            }
+
+            return apiService.createPlans(subscriptions, callContext);
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+    }
+
+    @Override
     public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
 
         final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
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 2e829f3..6e4b949 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
@@ -19,8 +19,10 @@
 package org.killbill.billing.subscription.api.user;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -70,7 +72,9 @@ import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.clock.Clock;
 import org.killbill.clock.DefaultClock;
 
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
@@ -104,6 +108,52 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         return subscription;
     }
 
+    @Override
+    public DefaultSubscriptionBase createPlans(final Iterable<SubscriptionSpecifier> subscriptions, final CallContext context) throws SubscriptionBaseApiException {
+
+        Map<UUID, List<SubscriptionBaseEvent>> eventsMap = new HashMap<UUID, List<SubscriptionBaseEvent>>();
+        List<DefaultSubscriptionBase> subscriptionBaseList = new ArrayList<DefaultSubscriptionBase>();
+        for (SubscriptionSpecifier subscription : subscriptions) {
+
+            try {
+                final DefaultSubscriptionBase subscriptionBase = new DefaultSubscriptionBase(subscription.getBuilder(), this, clock);
+                final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBase.getBundleId(), context);
+                final List<SubscriptionBaseEvent> events = getEventsOnCreation(subscriptionBase.getBundleId(), subscriptionBase.getId(), subscriptionBase.getAlignStartDate(),
+                                                                               subscriptionBase.getBundleStartDate(), subscriptionBase.getActiveVersion(), subscription.getPlan(),
+                                                                               subscription.getInitialPhase(), subscription.getRealPriceList(), subscription.getRequestedDate(),
+                                                                               subscription.getEffectiveDate(), subscription.getProcessedDate(), false, internalCallContext);
+
+                eventsMap.put(subscriptionBase.getId(), events);
+                subscriptionBaseList.add(subscriptionBase);
+
+            } catch (final CatalogApiException e) {
+                throw new SubscriptionBaseApiException(e);
+            }
+        }
+
+        final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBaseList.get(0).getBundleId(), context);
+        dao.createSubscriptionWithAddOns(subscriptionBaseList, eventsMap, internalCallContext);
+
+        final DefaultSubscriptionBase baseSubscription = findBaseSubscription(subscriptionBaseList);
+        try {
+            baseSubscription.rebuildTransitions(dao.getEventsForSubscription(baseSubscription.getId(), internalCallContext),
+                                                catalogService.getFullCatalog(internalCallContext));
+        } catch (CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+
+        return baseSubscription;
+    }
+
+    private DefaultSubscriptionBase findBaseSubscription(final List<DefaultSubscriptionBase> subscriptionBaseList) {
+        return Iterables.tryFind(subscriptionBaseList, new Predicate<DefaultSubscriptionBase>() {
+            @Override
+            public boolean apply(final DefaultSubscriptionBase subscription) {
+                return ProductCategory.BASE.equals(subscription.getCategory());
+            }
+        }).orNull();
+    }
+
     @Deprecated
     @Override
     public boolean recreatePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDateWithMs, final CallContext context)
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionSpecifier.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionSpecifier.java
new file mode 100644
index 0000000..154ee62
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionSpecifier.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.subscription.api.user;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+
+public class SubscriptionSpecifier {
+
+    private SubscriptionBuilder builder;
+    private Plan plan;
+    private PhaseType initialPhase;
+    private String realPriceList;
+    private DateTime requestedDate;
+    private DateTime effectiveDate;
+    private DateTime processedDate;
+
+    public SubscriptionSpecifier() {
+    }
+
+    public SubscriptionSpecifier(final SubscriptionBuilder builder, final Plan plan,
+                                 final PhaseType initialPhase, final String realPriceList,
+                                 final DateTime requestedDate, final DateTime effectiveDate,
+                                 final DateTime processedDate) {
+        this.builder = builder;
+        this.plan = plan;
+        this.initialPhase = initialPhase;
+        this.realPriceList = realPriceList;
+        this.requestedDate = requestedDate;
+        this.effectiveDate = effectiveDate;
+        this.processedDate = processedDate;
+    }
+
+    public SubscriptionBuilder getBuilder() {
+        return builder;
+    }
+
+    public void setBuilder(final SubscriptionBuilder builder) {
+        this.builder = builder;
+    }
+
+    public Plan getPlan() {
+        return plan;
+    }
+
+    public void setPlan(final Plan plan) {
+        this.plan = plan;
+    }
+
+    public PhaseType getInitialPhase() {
+        return initialPhase;
+    }
+
+    public void setInitialPhase(final PhaseType initialPhase) {
+        this.initialPhase = initialPhase;
+    }
+
+    public String getRealPriceList() {
+        return realPriceList;
+    }
+
+    public void setRealPriceList(final String realPriceList) {
+        this.realPriceList = realPriceList;
+    }
+
+    public DateTime getRequestedDate() {
+        return requestedDate;
+    }
+
+    public void setRequestedDate(final DateTime requestedDate) {
+        this.requestedDate = requestedDate;
+    }
+
+    public DateTime getEffectiveDate() {
+        return effectiveDate;
+    }
+
+    public void setEffectiveDate(final DateTime effectiveDate) {
+        this.effectiveDate = effectiveDate;
+    }
+
+    public DateTime getProcessedDate() {
+        return processedDate;
+    }
+
+    public void setProcessedDate(final DateTime processedDate) {
+        this.processedDate = processedDate;
+    }
+
+}
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 63d5d56..4a72049 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
@@ -510,6 +510,35 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
     }
 
     @Override
+    public void createSubscriptionWithAddOns(final List<DefaultSubscriptionBase> subscriptions, final Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap, final InternalCallContext context) {
+        transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+            @Override
+            public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+                final SubscriptionSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class);
+                final SubscriptionEventSqlDao eventsDaoFromSameTransaction = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
+
+                for (DefaultSubscriptionBase subscription : subscriptions) {
+                    transactional.create(new SubscriptionModelDao(subscription), context);
+
+                    final List<SubscriptionBaseEvent> initialEvents = initialEventsMap.get(subscription.getId());
+                    for (final SubscriptionBaseEvent cur : initialEvents) {
+                        eventsDaoFromSameTransaction.create(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);
+
+                    }
+                    // Notify the Bus of the latest requested change, if needed
+                    if (initialEvents.size() > 0) {
+                        notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, initialEvents.get(initialEvents.size() - 1), SubscriptionBaseTransitionType.CREATE, context);
+                    }
+                }
+                return null;
+            }
+        });
+    }
+
+    @Override
     public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 13cfa2b..893c5d4 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -82,6 +82,8 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
     // SubscriptionBase creation, cancellation, changePlanWithRequestedDate apis
     public void createSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> initialEvents, InternalCallContext context);
 
+    public void createSubscriptionWithAddOns(List<DefaultSubscriptionBase> subscriptions, Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap, InternalCallContext context);
+
     public void recreateSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> recreateEvents, InternalCallContext context);
 
     public void cancelSubscription(DefaultSubscriptionBase subscription, SubscriptionBaseEvent cancelEvent, InternalCallContext context, int cancelSeq);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 1d95133..6aaed33 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -213,6 +213,24 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
     }
 
     @Override
+    public void createSubscriptionWithAddOns(final List<DefaultSubscriptionBase> subscriptions,
+                                             final Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap,
+                                             final InternalCallContext context) {
+        synchronized (events) {
+            for (DefaultSubscriptionBase subscription : subscriptions) {
+                final List<SubscriptionBaseEvent> initialEvents = initialEventsMap.get(subscription.getId());
+                events.addAll(initialEvents);
+                for (final SubscriptionBaseEvent cur : initialEvents) {
+                    recordFutureNotificationFromTransaction(null, cur.getEffectiveDate(), new SubscriptionNotificationKey(cur.getId()), context);
+                }
+                final SubscriptionBase updatedSubscription = buildSubscription(subscription, context);
+                this.subscriptions.add(updatedSubscription);
+                mockNonEntityDao.addTenantRecordIdMapping(updatedSubscription.getId(), context);
+            }
+        }
+    }
+
+    @Override
     public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
         synchronized (events) {
             events.addAll(recreateEvents);
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
index 9b12658..be25ccd 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
@@ -23,9 +23,11 @@ import java.util.Locale;
 import javax.inject.Inject;
 import javax.inject.Named;
 
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.tenant.api.TenantKV.TenantKey;
 import org.killbill.billing.tenant.dao.TenantDao;
+import org.killbill.billing.tenant.dao.TenantModelDao;
 import org.killbill.billing.tenant.glue.DefaultTenantModule;
 import org.killbill.billing.util.LocaleUtils;
 
@@ -106,6 +108,16 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
     }
 
 
+    @Override
+    public Tenant getTenantByApiKey(final String key) throws TenantApiException {
+        final TenantModelDao tenant = tenantDao.getTenantByApiKey(key);
+        if (tenant == null) {
+            throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_API_KEY, key);
+        }
+        return new DefaultTenant(tenant);
+    }
+
+
 
     private String getUniqueValue(final List<String> values, final String msg, final InternalTenantContext tenantContext) {
         if (values.isEmpty()) {
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 f1fe45c..3c1c575 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
@@ -67,12 +67,15 @@ 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;
+
 
     @Inject
     public DefaultTenantUserApi(final TenantDao tenantDao, final InternalCallContextFactory internalCallContextFactory, final CacheControllerDispatcher cacheControllerDispatcher) {
         this.tenantDao = tenantDao;
         this.internalCallContextFactory = internalCallContextFactory;
         this.tenantKVCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT_KV);
+        this.tenantCache = cacheControllerDispatcher.getCacheController(CacheType.TENANT);
     }
 
     @Override
@@ -84,17 +87,16 @@ public class DefaultTenantUserApi implements TenantUserApi {
         } catch (final TenantApiException e) {
             throw new TenantApiException(e, ErrorCode.TENANT_CREATION_FAILED);
         }
-
         return tenant;
     }
 
     @Override
     public Tenant getTenantByApiKey(final String key) throws TenantApiException {
-        final TenantModelDao tenant = tenantDao.getTenantByApiKey(key);
+        final Tenant tenant = (Tenant) tenantCache.get(key, new CacheLoaderArgument(ObjectType.TENANT));
         if (tenant == null) {
             throw new TenantApiException(ErrorCode.TENANT_DOES_NOT_EXIST_FOR_API_KEY, key);
         }
-        return new DefaultTenant(tenant);
+        return tenant;
     }
 
     @Override
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/NoCachingTenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/NoCachingTenantDao.java
index d7cb4d5..a491f09 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/dao/NoCachingTenantDao.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/NoCachingTenantDao.java
@@ -56,8 +56,13 @@ public class NoCachingTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Te
     }
 
     @Override
-    public TenantModelDao getTenantByApiKey(final String key) {
-        throw new IllegalStateException("Not implemented by NoCachingTenantDao");
+    public TenantModelDao getTenantByApiKey(final String apiKey) {
+        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<TenantModelDao>() {
+            @Override
+            public TenantModelDao inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+                return entitySqlDaoWrapperFactory.become(TenantSqlDao.class).getByApiKey(apiKey);
+            }
+        });
     }
 
     @Override
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java
index 1f71b00..cf97271 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/TenantSqlDao.java
@@ -16,6 +16,9 @@
 
 package org.killbill.billing.tenant.dao;
 
+import org.killbill.billing.util.cache.Cachable;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+import org.killbill.billing.util.cache.CachableKey;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/api/user/TestDefaultTenantUserApi.java b/tenant/src/test/java/org/killbill/billing/tenant/api/user/TestDefaultTenantUserApi.java
index 9d70464..2d4faa4 100644
--- a/tenant/src/test/java/org/killbill/billing/tenant/api/user/TestDefaultTenantUserApi.java
+++ b/tenant/src/test/java/org/killbill/billing/tenant/api/user/TestDefaultTenantUserApi.java
@@ -18,14 +18,37 @@
 package org.killbill.billing.tenant.api.user;
 
 import java.util.List;
+import java.util.UUID;
 
 import org.killbill.billing.tenant.TenantTestSuiteWithEmbeddedDb;
+import org.killbill.billing.tenant.api.DefaultTenant;
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantData;
 import org.killbill.billing.tenant.api.TenantKV.TenantKey;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 public class TestDefaultTenantUserApi extends TenantTestSuiteWithEmbeddedDb {
 
+
+    @Test(groups = "slow")
+    public void testTenant() throws Exception {
+        final TenantData tenantdata = new DefaultTenant(UUID.randomUUID(), clock.getUTCNow(), clock.getUTCNow(), "er44TT-yy4r", "TTR445ee2", "dskjhfs^^54R");
+        tenantUserApi.createTenant(tenantdata, callContext);
+
+        final Tenant tenant = tenantUserApi.getTenantByApiKey(tenantdata.getApiKey());
+
+        Assert.assertEquals(tenant.getApiKey(), tenantdata.getApiKey());
+        Assert.assertEquals(tenant.getExternalKey(), tenantdata.getExternalKey());
+
+
+        // The second time, the value is already in the cache so the TenantCacheLoader is not invoked
+        final Tenant tenant2 = tenantUserApi.getTenantByApiKey(tenantdata.getApiKey());
+
+        Assert.assertEquals(tenant2.getApiKey(), tenantdata.getApiKey());
+        Assert.assertEquals(tenant2.getExternalKey(), tenantdata.getExternalKey());
+    }
+
     @Test(groups = "slow")
     public void testUserKey() throws Exception {
         tenantUserApi.addTenantKeyValue("THE_KEY", "TheValue", callContext);
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 bfc0b27..6035ff3 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
@@ -34,6 +34,7 @@ public @interface Cachable {
     String TENANT_CATALOG_CACHE_NAME = "tenant-catalog";
     String TENANT_OVERDUE_CONFIG_CACHE_NAME = "tenant-overdue-config";
     String TENANT_KV_CACHE_NAME = "tenant-kv";
+    String TENANT_CACHE_NAME = "tenant";
     String OVERRIDDEN_PLAN_CACHE_NAME = "overridden-plan";
     String ACCOUNT_IMMUTABLE_CACHE_NAME = "account-immutable";
     String ACCOUNT_BCD_CACHE_NAME = "account-bcd";
@@ -66,9 +67,12 @@ public @interface Cachable {
         /* Tenant overdue config cache */
         TENANT_OVERDUE_CONFIG(TENANT_OVERDUE_CONFIG_CACHE_NAME, false),
 
-        /* Tenant overdue config cache */
+        /* Tenant config cache */
         TENANT_KV(TENANT_KV_CACHE_NAME, false),
 
+        /* Tenant config cache */
+        TENANT(TENANT_CACHE_NAME, false),
+
         /* Overwritten plans  */
         OVERRIDDEN_PLAN(OVERRIDDEN_PLAN_CACHE_NAME, false),
 
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
index 269ff66..f90b1d1 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheCacheManagerProvider.java
@@ -62,6 +62,7 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
                                        final TenantCatalogCacheLoader tenantCatalogCacheLoader,
                                        final TenantOverdueConfigCacheLoader tenantOverdueConfigCacheLoader,
                                        final TenantKVCacheLoader tenantKVCacheLoader,
+                                       final TenantCacheLoader tenantCacheLoader,
                                        final OverriddenPlanCacheLoader overriddenPlanCacheLoader) {
         this.metricRegistry = metricRegistry;
         this.cacheConfig = cacheConfig;
@@ -76,6 +77,7 @@ public class EhCacheCacheManagerProvider implements Provider<CacheManager> {
         cacheLoaders.add(tenantCatalogCacheLoader);
         cacheLoaders.add(tenantOverdueConfigCacheLoader);
         cacheLoaders.add(tenantKVCacheLoader);
+        cacheLoaders.add(tenantCacheLoader);
         cacheLoaders.add(overriddenPlanCacheLoader);
     }
 
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
new file mode 100644
index 0000000..bacafa8
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/cache/TenantCacheLoader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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 javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.killbill.billing.tenant.api.Tenant;
+import org.killbill.billing.tenant.api.TenantApiException;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.util.cache.Cachable.CacheType;
+
+@Singleton
+public class TenantCacheLoader extends BaseCacheLoader {
+
+    private final TenantInternalApi tenantApi;
+
+    @Inject
+    public TenantCacheLoader(final TenantInternalApi tenantApi) {
+        super();
+        this.tenantApi = tenantApi;
+    }
+
+    @Override
+    public CacheType getCacheType() {
+        return CacheType.TENANT;
+    }
+
+    @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());
+        }
+
+        try {
+            return tenantApi.getTenantByApiKey((String) key);
+        } catch (TenantApiException e) {
+            throw new IllegalStateException("TenantCacheLoader cannot find value for key " + key);
+        }
+    }
+}
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 d758e18..043805e 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
@@ -175,7 +175,7 @@ public class DefaultNonEntityDao implements NonEntityDao {
 
         private TypeOut withCaching(final OperationRetrieval<TypeIn, TypeOut> op, @Nullable final TypeIn objectOrRecordId, final ObjectType objectType, final TableName tableName, @Nullable final CacheController<Object, Object> cache) {
 
-            final Profiling<TypeOut> prof = new Profiling<TypeOut>();
+            final Profiling<TypeOut, RuntimeException> prof = new Profiling<TypeOut, RuntimeException>();
             if (objectOrRecordId == null) {
                 return null;
             }
@@ -186,18 +186,13 @@ public class DefaultNonEntityDao implements NonEntityDao {
                 return (TypeOut) cache.get(key, new CacheLoaderArgument(objectType));
             }
             final TypeOut result;
-            try {
-                result = prof.executeWithProfiling(ProfilingFeatureType.DAO_DETAILS, "NonEntityDao (type = " + objectType + ") cache miss", new WithProfilingCallback<TypeOut>() {
-                    @Override
-                    public <ExceptionType extends Throwable> TypeOut execute() throws ExceptionType {
-                        return op.doRetrieve(objectOrRecordId, objectType);
-                    }
-                });
-                return result;
-            } catch (final Throwable throwable) {
-                // This is only because WithProfilingCallback throws a Throwable...
-                throw new RuntimeException(throwable);
-            }
+            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 result;
         }
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java b/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java
index b944c0d..798b334 100644
--- a/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/dao/NonEntitySqlDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
  *
@@ -18,17 +20,16 @@ package org.killbill.billing.util.dao;
 
 import java.util.UUID;
 
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.BindBean;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.customizers.Define;
 import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
-
-import org.killbill.billing.callcontext.InternalTenantContext;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface NonEntitySqlDao extends Transactional<NonEntitySqlDao>, CloseMe {
 
     @SqlQuery
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 d128899..9793364 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
@@ -104,7 +104,7 @@ public class EntitySqlDaoWrapperInvocationHandler<S extends EntitySqlDao<M, E>, 
         this.clock = clock;
         this.cacheControllerDispatcher = cacheControllerDispatcher;
         this.nonEntityDao = nonEntityDao;
-        this.prof = new Profiling<Object>();
+        this.prof = new Profiling<Object, Throwable>();
     }
 
     @Override
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 5b94fb4..f01e061 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
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
diff --git a/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
index d8b92a7..5603b73 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/EhCacheManagerProvider.java
@@ -24,27 +24,16 @@ import javax.inject.Provider;
 import org.apache.shiro.cache.ehcache.EhCacheManager;
 import org.apache.shiro.mgt.DefaultSecurityManager;
 import org.apache.shiro.mgt.SecurityManager;
-import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import com.codahale.metrics.MetricRegistry;
-import com.codahale.metrics.ehcache.InstrumentedEhcache;
-import net.sf.ehcache.CacheException;
 import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
 
 public class EhCacheManagerProvider implements Provider<EhCacheManager> {
 
-    private static final Logger logger = LoggerFactory.getLogger(EhCacheManagerProvider.class);
-
-    private final MetricRegistry metricRegistry;
     private final SecurityManager securityManager;
     private final CacheManager ehCacheCacheManager;
 
     @Inject
-    public EhCacheManagerProvider(final MetricRegistry metricRegistry, final SecurityManager securityManager, final CacheManager ehCacheCacheManager) {
-        this.metricRegistry = metricRegistry;
+    public EhCacheManagerProvider(final SecurityManager securityManager, final CacheManager ehCacheCacheManager) {
         this.securityManager = securityManager;
         this.ehCacheCacheManager = ehCacheCacheManager;
     }
@@ -55,21 +44,8 @@ public class EhCacheManagerProvider implements Provider<EhCacheManager> {
         // Same EhCache manager instance as the rest of the system
         shiroEhCacheManager.setCacheManager(ehCacheCacheManager);
 
-        // It looks like Shiro's cache manager is not thread safe. Concurrent requests on startup
-        // can throw org.apache.shiro.cache.CacheException: net.sf.ehcache.ObjectExistsException: Cache shiro-activeSessionCache already exists
-        // As a workaround, create the cache manually here
-        shiroEhCacheManager.getCache(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
-
-        // Instrument the cache
-        final Ehcache shiroActiveSessionEhcache = ehCacheCacheManager.getEhcache(CachingSessionDAO.ACTIVE_SESSION_CACHE_NAME);
-        final Ehcache decoratedCache = InstrumentedEhcache.instrument(metricRegistry, shiroActiveSessionEhcache);
-        try {
-            ehCacheCacheManager.replaceCacheWithDecoratedCache(shiroActiveSessionEhcache, decoratedCache);
-        } catch (final CacheException e) {
-            logger.warn("Unable to instrument cache {}: {}", shiroActiveSessionEhcache.getName(), e.getMessage());
-        }
-
         if (securityManager instanceof DefaultSecurityManager) {
+            // For RBAC only (see also KillbillJdbcTenantRealmProvider)
             ((DefaultSecurityManager) securityManager).setCacheManager(shiroEhCacheManager);
         }
 
diff --git a/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
index 379e3b6..8e74e76 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/IniRealmProvider.java
@@ -53,17 +53,26 @@ public class IniRealmProvider implements Provider<IniRealm> {
             // by going through IniSecurityManagerFactory.
             final DefaultSecurityManager securityManager = (DefaultSecurityManager) factory.getInstance();
             final Collection<Realm> realms = securityManager.getRealms();
-            if (realms == null || realms.isEmpty()) {
-                return new IniRealm(securityConfig.getShiroResourcePath());
-            }
 
-            for (final Realm cur : realms) {
-                if (cur instanceof IniRealm) {
-                    return (IniRealm) cur;
+            IniRealm iniRealm = null;
+            if (realms == null || realms.isEmpty()) {
+                iniRealm = new IniRealm(securityConfig.getShiroResourcePath());
+            } else {
+                for (final Realm cur : realms) {
+                    if (cur instanceof IniRealm) {
+                        iniRealm = (IniRealm) cur;
+                        break;
+                    }
                 }
             }
-            throw new ConfigurationException();
+            if (iniRealm != null) {
+                // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+                iniRealm.setAuthenticationCachingEnabled(true);
 
+                return iniRealm;
+            } else {
+                throw new ConfigurationException();
+            }
         } catch (final ConfigurationException e) {
             log.warn("Unable to configure RBAC", e);
             return new IniRealm();
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
index 82f2467..68c6c04 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillbillApiAopModule.java
@@ -42,7 +42,7 @@ public class KillbillApiAopModule extends AbstractModule {
 
     public static class ProfilingMethodInterceptor implements MethodInterceptor {
 
-        private final Profiling prof = new Profiling<Object>();
+        private final Profiling prof = new Profiling<Object, Throwable>();
 
         @Override
         public Object invoke(final MethodInvocation invocation) throws Throwable {
diff --git a/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java b/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java
new file mode 100644
index 0000000..6a0bb91
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/glue/ShiroEhCacheInstrumentor.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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 javax.inject.Inject;
+
+import org.apache.shiro.cache.CacheManager;
+import org.apache.shiro.realm.AuthenticatingRealm;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.realm.Realm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ehcache.InstrumentedEhcache;
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Ehcache;
+
+public class ShiroEhCacheInstrumentor {
+
+    private static final Logger logger = LoggerFactory.getLogger(ShiroEhCacheInstrumentor.class);
+
+    private final MetricRegistry metricRegistry;
+    private final CacheManager shiroEhCacheManager;
+    private final net.sf.ehcache.CacheManager ehCacheCacheManager;
+
+    @Inject
+    public ShiroEhCacheInstrumentor(final MetricRegistry metricRegistry, final CacheManager shiroEhCacheManager, final net.sf.ehcache.CacheManager ehCacheCacheManager) {
+        this.metricRegistry = metricRegistry;
+        this.shiroEhCacheManager = shiroEhCacheManager;
+        this.ehCacheCacheManager = ehCacheCacheManager;
+    }
+
+    public void instrument(final Realm realm) {
+        if (realm instanceof AuthorizingRealm) {
+            instrument((AuthorizingRealm) realm);
+        } else if (realm instanceof AuthenticatingRealm) {
+            instrument((AuthenticatingRealm) realm);
+        }
+    }
+
+    public void instrument(final AuthorizingRealm realm) {
+        instrument(realm.getAuthenticationCacheName());
+        instrument(realm.getAuthorizationCacheName());
+    }
+
+    public void instrument(final AuthenticatingRealm realm) {
+        instrument(realm.getAuthenticationCacheName());
+    }
+
+    public void instrument(final String cacheName) {
+        // Initialize the cache, if it doesn't exist yet
+        // Note: Shiro's cache manager is not thread safe. Concurrent requests on startup
+        // can throw org.apache.shiro.cache.CacheException: net.sf.ehcache.ObjectExistsException: Cache shiro-activeSessionCache already exists
+        shiroEhCacheManager.getCache(cacheName);
+
+        final Ehcache shiroEhcache = ehCacheCacheManager.getEhcache(cacheName);
+        final Ehcache decoratedCache = InstrumentedEhcache.instrument(metricRegistry, shiroEhcache);
+        try {
+            ehCacheCacheManager.replaceCacheWithDecoratedCache(shiroEhcache, decoratedCache);
+        } catch (final CacheException e) {
+            logger.warn("Unable to instrument cache {}: {}", shiroEhcache.getName(), e.getMessage());
+        }
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java
index ea99854..68000d9 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/JDBCSessionSqlDao.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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,14 +19,14 @@
 package org.killbill.billing.util.security.shiro.dao;
 
 import org.joda.time.DateTime;
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface JDBCSessionSqlDao extends Transactional<JDBCSessionSqlDao> {
 
     @SqlQuery
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RolesPermissionsSqlDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RolesPermissionsSqlDao.java
index 0e25943..029b268 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RolesPermissionsSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/RolesPermissionsSqlDao.java
@@ -19,14 +19,14 @@ package org.killbill.billing.util.security.shiro.dao;
 
 import java.util.List;
 
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface RolesPermissionsSqlDao extends Transactional<RolesPermissionsSqlDao> {
 
     @SqlQuery
@@ -37,5 +37,4 @@ public interface RolesPermissionsSqlDao extends Transactional<RolesPermissionsSq
 
     @SqlUpdate
     public void create(@SmartBindBean final RolesPermissionsModelDao rolesPermissions);
-
 }
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UserRolesSqlDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UserRolesSqlDao.java
index c59d80c..8c25d9b 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UserRolesSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UserRolesSqlDao.java
@@ -20,14 +20,14 @@ package org.killbill.billing.util.security.shiro.dao;
 import java.util.Date;
 import java.util.List;
 
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface UserRolesSqlDao extends Transactional<UserRolesSqlDao> {
 
     @SqlQuery
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UsersSqlDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UsersSqlDao.java
index f7f6d16..6c7cd31 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UsersSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/UsersSqlDao.java
@@ -19,14 +19,14 @@ package org.killbill.billing.util.security.shiro.dao;
 
 import java.util.Date;
 
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.commons.jdbi.binder.SmartBindBean;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 public interface UsersSqlDao extends Transactional<UsersSqlDao> {
 
     @SqlQuery
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
index 380258b..87c8588 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
@@ -42,6 +42,10 @@ public class KillBillJdbcRealm extends JdbcRealm {
         this.dataSource = dataSource;
         this.securityConfig = securityConfig;
 
+        // TODO Enable when we add support for cache invalidation
+        // See JavaDoc warning: https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/AuthenticatingRealm.html
+        //setAuthenticationCachingEnabled(true);
+
         // Tweak JdbcRealm defaults
         setPermissionsLookupEnabled(true);
         setAuthenticationQuery(KILLBILL_SALTED_AUTHENTICATION_QUERY);
diff --git a/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java
index bcc7095..e9007fb 100644
--- a/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java
+++ b/util/src/main/java/org/killbill/billing/util/validation/dao/DatabaseSchemaSqlDao.java
@@ -1,7 +1,9 @@
 /*
- * Copyright 2010-2011 Ning, Inc.
+ * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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,15 +24,15 @@ import java.util.List;
 
 import javax.annotation.Nullable;
 
+import org.killbill.billing.util.entity.dao.EntitySqlDaoStringTemplate;
 import org.killbill.billing.util.validation.DefaultColumnInfo;
 import org.skife.jdbi.v2.StatementContext;
 import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper;
-import org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator;
 import org.skife.jdbi.v2.tweak.ResultSetMapper;
 
-@UseStringTemplate3StatementLocator
+@EntitySqlDaoStringTemplate
 @RegisterMapper(DatabaseSchemaSqlDao.ColumnInfoMapper.class)
 public interface DatabaseSchemaSqlDao {
 
diff --git a/util/src/main/resources/ehcache.xml b/util/src/main/resources/ehcache.xml
index d172871..ced3336 100644
--- a/util/src/main/resources/ehcache.xml
+++ b/util/src/main/resources/ehcache.xml
@@ -195,5 +195,19 @@
     </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>
+
+
 </ehcache>
 
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestDateInvestigation.java b/util/src/test/java/org/killbill/billing/util/dao/TestDateInvestigation.java
index 898794c..482ed06 100644
--- a/util/src/test/java/org/killbill/billing/util/dao/TestDateInvestigation.java
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestDateInvestigation.java
@@ -68,7 +68,7 @@ public class TestDateInvestigation /* extends UtilTestSuiteWithEmbeddedDB */ {
     }
 
     @BeforeTest(groups = "slow")
-    public void beforeTest() {
+    public void beforeTest() throws SQLException {
         rawSource = getRawSource(DataSourceType.MYSQL_MARIADB, "killbill", "root", "root");
     }
 
@@ -171,7 +171,7 @@ public class TestDateInvestigation /* extends UtilTestSuiteWithEmbeddedDB */ {
         }
     }
 
-    private DataSource getRawSource(final DataSourceType type, final String dbName, final String user, final String pwd) {
+    private DataSource getRawSource(final DataSourceType type, final String dbName, final String user, final String pwd) throws SQLException {
         if (type == DataSourceType.MYSQL_JDBC2) {
             return getRawMysqlDataSource(dbName, user, pwd);
         } else if (type == DataSourceType.MYSQL_MARIADB) {
@@ -191,13 +191,13 @@ public class TestDateInvestigation /* extends UtilTestSuiteWithEmbeddedDB */ {
         return rawSource;
     }
 
-    private DataSource getRawMariaDBDataSource(final String dbName, final String user, final String pwd) {
+    private DataSource getRawMariaDBDataSource(final String dbName, final String user, final String pwd) throws SQLException {
         final org.mariadb.jdbc.MySQLDataSource rawSource = new org.mariadb.jdbc.MySQLDataSource();
+        rawSource.setURL("jdbc:mysql://localhost:3306/killbill?createDatabaseIfNotExist=true&allowMultiQueries=true");
         rawSource.setDatabaseName(dbName);
         rawSource.setUser(user);
         rawSource.setPassword(pwd);
         rawSource.setPort(3306);
-        rawSource.setURL("jdbc:mysql://localhost:3306/killbill?createDatabaseIfNotExist=true&allowMultiQueries=true");
         return rawSource;
     }