killbill-aplcache

Changes

.idea/libraries/Maven__com_ning_billing_killbill_api_0_6_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_billing_plugin_killbill_plugin_api_notification_0_4_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_billing_plugin_killbill_plugin_api_payment_0_4_0.xml 13(+0 -13)

.travis.yml 2(+1 -1)

pom.xml 2(+1 -1)

Details

.travis.yml 2(+1 -1)

diff --git a/.travis.yml b/.travis.yml
index add54e4..6283386 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,7 @@ install: mvn install -DskipTests=true
 
 notifications:
   email:
-    - killbilling-dev@googlegroups.com
+    - kill-bill-commits@googlegroups.com
 
 jdk:
   - openjdk6
diff --git a/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
index 5f434c7..486057d 100644
--- a/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -26,6 +26,7 @@ import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.account.api.AccountEmail;
+import com.ning.billing.account.api.AccountInternalApi;
 import com.ning.billing.account.api.DefaultAccount;
 import com.ning.billing.account.api.DefaultAccountEmail;
 import com.ning.billing.account.dao.AccountDao;
@@ -33,11 +34,13 @@ import com.ning.billing.account.dao.AccountEmailModelDao;
 import com.ning.billing.account.dao.AccountModelDao;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
-import com.ning.billing.account.api.AccountInternalApi;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 
 public class DefaultAccountInternalApi implements AccountInternalApi {
 
@@ -64,6 +67,21 @@ public class DefaultAccountInternalApi implements AccountInternalApi {
     }
 
     @Override
+    public Pagination<Account> getAccounts(final Long offset, final Long limit, final InternalTenantContext context) {
+        final Pagination<AccountModelDao> accountModelDaos = accountDao.get(offset, limit, context);
+        return new DefaultPagination<Account>(accountModelDaos,
+                                              limit,
+                                              Iterators.<AccountModelDao, Account>transform(accountModelDaos.iterator(),
+                                                                                            new Function<AccountModelDao, Account>() {
+                                                                                                @Override
+                                                                                                public Account apply(final AccountModelDao input) {
+                                                                                                    return new DefaultAccount(input);
+                                                                                                }
+                                                                                            }));
+
+    }
+
+    @Override
     public void updateAccount(final String externalKey, final AccountData accountData,
                               final InternalCallContext context) throws AccountApiException {
         final Account currentAccount = getAccountByKey(externalKey, context);
diff --git a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
index 96d6191..d1ffe58 100644
--- a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
+++ b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
@@ -19,8 +19,6 @@ package com.ning.billing.account.api.user;
 import java.util.List;
 import java.util.UUID;
 
-import javax.annotation.Nullable;
-
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
@@ -36,10 +34,13 @@ import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallContextFactory;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
 public class DefaultAccountUserApi implements AccountUserApi {
@@ -90,25 +91,31 @@ public class DefaultAccountUserApi implements AccountUserApi {
     }
 
     @Override
-    public List<Account> searchAccounts(final String searchKey, final TenantContext context) {
-        final List<AccountModelDao> accountModelDaos = accountDao.searchAccounts(searchKey, internalCallContextFactory.createInternalTenantContext(context));
-        return ImmutableList.<Account>copyOf(Collections2.transform(accountModelDaos, new Function<AccountModelDao, Account>() {
-            @Override
-            public Account apply(final AccountModelDao input) {
-                return new DefaultAccount(input);
-            }
-        }));
+    public Pagination<Account> searchAccounts(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+        final Pagination<AccountModelDao> accountModelDaos = accountDao.searchAccounts(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+        return new DefaultPagination<Account>(accountModelDaos,
+                                              limit,
+                                              Iterators.<AccountModelDao, Account>transform(accountModelDaos.iterator(),
+                                                                                            new Function<AccountModelDao, Account>() {
+                                                                                                @Override
+                                                                                                public Account apply(final AccountModelDao input) {
+                                                                                                    return new DefaultAccount(input);
+                                                                                                }
+                                                                                            }));
     }
 
     @Override
-    public List<Account> getAccounts(final TenantContext context) {
-        final List<AccountModelDao> accountModelDaos = accountDao.get(internalCallContextFactory.createInternalTenantContext(context));
-        return ImmutableList.<Account>copyOf(Collections2.transform(accountModelDaos, new Function<AccountModelDao, Account>() {
-            @Override
-            public Account apply(@Nullable final AccountModelDao input) {
-                return new DefaultAccount(input);
-            }
-        }));
+    public Pagination<Account> getAccounts(final Long offset, final Long limit, final TenantContext context) {
+        final Pagination<AccountModelDao> accountModelDaos = accountDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+        return new DefaultPagination<Account>(accountModelDaos,
+                                              limit,
+                                              Iterators.<AccountModelDao, Account>transform(accountModelDaos.iterator(),
+                                                                                            new Function<AccountModelDao, Account>() {
+                                                                                                @Override
+                                                                                                public Account apply(final AccountModelDao input) {
+                                                                                                    return new DefaultAccount(input);
+                                                                                                }
+                                                                                            }));
     }
 
     @Override
diff --git a/account/src/main/java/com/ning/billing/account/dao/AccountDao.java b/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
index eb80519..fbe6bfc 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
@@ -23,13 +23,14 @@ import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.entity.dao.EntityDao;
 
 public interface AccountDao extends EntityDao<AccountModelDao, Account, AccountApiException> {
 
     public AccountModelDao getAccountByKey(String key, InternalTenantContext context);
 
-    public List<AccountModelDao> searchAccounts(String searchKey, InternalTenantContext context);
+    public Pagination<AccountModelDao> searchAccounts(String searchKey, Long offset, Long limit, InternalTenantContext context);
 
     /**
      * @throws AccountApiException when externalKey is null
@@ -49,6 +50,4 @@ public interface AccountDao extends EntityDao<AccountModelDao, Account, AccountA
     public void removeEmail(AccountEmailModelDao email, InternalCallContext context);
 
     public List<AccountEmailModelDao> getEmailsByAccountId(UUID accountId, InternalTenantContext context);
-
-    public AccountModelDao getByRecordId(Long recordId, InternalCallContext context);
 }
diff --git a/account/src/main/java/com/ning/billing/account/dao/AccountSqlDao.java b/account/src/main/java/com/ning/billing/account/dao/AccountSqlDao.java
index ab38c74..4cfd258 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AccountSqlDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AccountSqlDao.java
@@ -16,7 +16,7 @@
 
 package com.ning.billing.account.dao;
 
-import java.util.List;
+import java.util.Iterator;
 import java.util.UUID;
 
 import org.skife.jdbi.v2.sqlobject.Bind;
@@ -24,11 +24,12 @@ 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.Define;
+import org.skife.jdbi.v2.sqlobject.customizers.FetchSize;
 
 import com.ning.billing.account.api.Account;
-import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.entity.dao.Audited;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoStringTemplate;
@@ -41,8 +42,13 @@ public interface AccountSqlDao extends EntitySqlDao<AccountModelDao, Account> {
                                            @BindBean final InternalTenantContext context);
 
     @SqlQuery
-    public List<AccountModelDao> searchAccounts(@Define("searchKey") final String searchKey,
-                                                @BindBean final InternalTenantContext context);
+    // Magic value to force MySQL to stream from the database
+    // See http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html (ResultSet)
+    @FetchSize(Integer.MIN_VALUE)
+    public Iterator<AccountModelDao> searchAccounts(@Define("searchKey") final String searchKey,
+                                                    @Bind("offset") final Long offset,
+                                                    @Bind("rowCount") final Long rowCount,
+                                                    @BindBean final InternalTenantContext context);
 
     @SqlQuery
     public UUID getIdFromKey(@Bind("externalKey") final String key,
diff --git a/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java b/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
index b73d2a5..fca3684 100644
--- a/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.account.dao;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -32,21 +33,23 @@ import com.ning.billing.account.api.user.DefaultAccountCreationEvent;
 import com.ning.billing.account.api.user.DefaultAccountCreationEvent.DefaultAccountData;
 import com.ning.billing.bus.api.PersistentBus;
 import com.ning.billing.bus.api.PersistentBus.EventBusException;
-import com.ning.billing.util.audit.ChangeType;
-import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.clock.Clock;
-import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.entity.EntityPersistenceException;
+import com.ning.billing.events.AccountChangeInternalEvent;
+import com.ning.billing.events.AccountCreationInternalEvent;
+import com.ning.billing.util.audit.ChangeType;
+import com.ning.billing.util.cache.CacheControllerDispatcher;
+import com.ning.billing.util.callcontext.InternalCallContextFactory;
+import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
-import com.ning.billing.events.AccountChangeInternalEvent;
-import com.ning.billing.events.AccountCreationInternalEvent;
 
 import com.google.inject.Inject;
 
@@ -104,13 +107,31 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
     }
 
     @Override
-    public List<AccountModelDao> searchAccounts(final String searchKey, final InternalTenantContext context) {
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<AccountModelDao>>() {
+    public Pagination<AccountModelDao> searchAccounts(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        // Note: the connection will be busy as we stream the results out: hence we cannot use
+        // SQL_CALC_FOUND_ROWS / FOUND_ROWS on the actual query.
+        // We still need to know the actual number of results, mainly for the UI so that it knows if it needs to fetch
+        // more pages. To do that, we perform a dummy search query with SQL_CALC_FOUND_ROWS (but limit 1).
+        final Long count = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
             @Override
-            public List<AccountModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
-                return entitySqlDaoWrapperFactory.become(AccountSqlDao.class).searchAccounts(searchKey, context);
+            public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+                final AccountSqlDao accountSqlDao = entitySqlDaoWrapperFactory.become(AccountSqlDao.class);
+                final Iterator<AccountModelDao> dumbIterator = accountSqlDao.searchAccounts(searchKey, offset, 1L, context);
+                // Make sure to go through the results to close the connection
+                while (dumbIterator.hasNext()) {
+                    dumbIterator.next();
+                }
+                return accountSqlDao.getFoundRows(context);
             }
         });
+
+        // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+        // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+        final AccountSqlDao accountSqlDao = transactionalSqlDao.onDemand(AccountSqlDao.class);
+        final Long totalCount = accountSqlDao.getCount(context);
+        final Iterator<AccountModelDao> results = accountSqlDao.searchAccounts(searchKey, offset, limit, context);
+
+        return new DefaultPagination<AccountModelDao>(offset, limit, count, totalCount, results);
     }
 
     @Override
@@ -236,13 +257,4 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
         });
     }
 
-    @Override
-    public AccountModelDao getByRecordId(final Long recordId, final InternalCallContext context) {
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<AccountModelDao>() {
-            @Override
-            public AccountModelDao inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
-                return entitySqlDaoWrapperFactory.become(AccountSqlDao.class).getByRecordId(recordId, context);
-            }
-        });
-    }
 }
diff --git a/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg b/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
index 630f1dc..328f846 100644
--- a/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
+++ b/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
@@ -86,22 +86,27 @@ getAccountByKey() ::= <<
     where external_key = :externalKey <AND_CHECK_TENANT()>;
 >>
 
-searchAccounts(searchKey) ::= <<
-select <allTableFields()>
-from accounts
-where name like ('%<searchKey>%') <AND_CHECK_TENANT()>
-union
-select <allTableFields()>
-from accounts
-where email like ('%<searchKey>%') <AND_CHECK_TENANT()>
-union
-select <allTableFields()>
-from accounts
-where external_key like ('%<searchKey>%') <AND_CHECK_TENANT()>
-union
-select <allTableFields()>
-from accounts
-where company_name like ('%<searchKey>%') <AND_CHECK_TENANT()>
+searchAccounts(searchKey, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS <allTableFields()>
+from (
+  select <allTableFields()>
+  from accounts
+  where name like ('%<searchKey>%') <AND_CHECK_TENANT()>
+  union
+  select <allTableFields()>
+  from accounts
+  where email like ('%<searchKey>%') <AND_CHECK_TENANT()>
+  union
+  select <allTableFields()>
+  from accounts
+  where external_key like ('%<searchKey>%') <AND_CHECK_TENANT()>
+  union
+  select <allTableFields()>
+  from accounts
+  where company_name like ('%<searchKey>%') <AND_CHECK_TENANT()>
+) results
+order by results.record_id
+limit :offset, :rowCount
 ;
 >>
 
diff --git a/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java b/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java
index b8b6161..3a3620a 100644
--- a/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java
+++ b/account/src/test/java/com/ning/billing/account/dao/MockAccountDao.java
@@ -36,14 +36,16 @@ import com.ning.billing.account.api.user.DefaultAccountCreationEvent.DefaultAcco
 import com.ning.billing.bus.api.PersistentBus;
 import com.ning.billing.bus.api.PersistentBus.EventBusException;
 import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.callcontext.InternalTenantContext;
-import com.ning.billing.util.entity.dao.MockEntityDaoBase;
 import com.ning.billing.events.AccountChangeInternalEvent;
+import com.ning.billing.util.callcontext.InternalCallContextFactory;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.MockEntityDaoBase;
 
 import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, AccountApiException> implements AccountDao {
@@ -79,7 +81,7 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
         final long tenantRecordId = context == null ? InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID
                                                     : context.getTenantRecordId();
         final AccountChangeInternalEvent changeEvent = new DefaultAccountChangeEvent(account.getId(), currentAccount, account,
-                accountRecordId, tenantRecordId, UUID.randomUUID()
+                                                                                     accountRecordId, tenantRecordId, UUID.randomUUID()
         );
         if (changeEvent.hasChanges()) {
             try {
@@ -103,9 +105,9 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
     }
 
     @Override
-    public List<AccountModelDao> searchAccounts(final String searchKey, final InternalTenantContext context) {
+    public Pagination<AccountModelDao> searchAccounts(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
         final List<AccountModelDao> results = new LinkedList<AccountModelDao>();
-        for (final AccountModelDao account : get(context)) {
+        for (final AccountModelDao account : getAll(context)) {
             if ((account.getName() != null && account.getName().contains(searchKey)) ||
                 (account.getEmail() != null && account.getEmail().contains(searchKey)) ||
                 (account.getExternalKey() != null && account.getExternalKey().contains(searchKey)) ||
@@ -113,7 +115,8 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
                 results.add(account);
             }
         }
-        return results;
+
+        return DefaultPagination.<AccountModelDao>build(offset, limit, results);
     }
 
     @Override
@@ -152,7 +155,7 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
 
     @Override
     public List<AccountEmailModelDao> getEmailsByAccountId(final UUID accountId, final InternalTenantContext context) {
-        return ImmutableList.<AccountEmailModelDao>copyOf(Collections2.filter(accountEmailSqlDao.get(context), new Predicate<AccountEmailModelDao>() {
+        return ImmutableList.<AccountEmailModelDao>copyOf(Iterables.<AccountEmailModelDao>filter(accountEmailSqlDao.getAll(context), new Predicate<AccountEmailModelDao>() {
             @Override
             public boolean apply(final AccountEmailModelDao input) {
                 return input.getAccountId().equals(accountId);
@@ -160,8 +163,4 @@ public class MockAccountDao extends MockEntityDaoBase<AccountModelDao, Account, 
         }));
     }
 
-    @Override
-    public AccountModelDao getByRecordId(final Long recordId, final InternalCallContext context) {
-        return null;
-    }
 }
diff --git a/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java b/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
index d407232..9fd099b 100644
--- a/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
+++ b/account/src/test/java/com/ning/billing/account/dao/TestAccountDao.java
@@ -44,17 +44,20 @@ import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.customfield.StringCustomField;
 import com.ning.billing.util.customfield.dao.CustomFieldModelDao;
 import com.ning.billing.util.dao.TableName;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.tag.DescriptiveTag;
 import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.dao.TagDefinitionModelDao;
 import com.ning.billing.util.tag.dao.TagModelDao;
 
+import com.google.common.collect.ImmutableList;
+
 import static com.ning.billing.account.AccountTestUtils.checkAccountsEqual;
 import static com.ning.billing.account.AccountTestUtils.createTestAccount;
 
 public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
 
-    @Test(groups = "slow", description="Test Account: basic DAO calls")
+    @Test(groups = "slow", description = "Test Account: basic DAO calls")
     public void testBasic() throws AccountApiException {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
@@ -68,7 +71,8 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         checkAccountsEqual(retrievedAccount, account);
 
         // Retrieve all
-        final List<AccountModelDao> all = accountDao.get(internalCallContext);
+        final Pagination<AccountModelDao> allAccounts = accountDao.getAll(internalCallContext);
+        final List<AccountModelDao> all = ImmutableList.<AccountModelDao>copyOf(allAccounts);
         Assert.assertNotNull(all);
         Assert.assertEquals(all.size(), 1);
         checkAccountsEqual(all.get(0), account);
@@ -80,7 +84,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     }
 
     // Simple test to ensure long phone numbers can be stored
-    @Test(groups = "slow", description="Test Account DAO: long numbers")
+    @Test(groups = "slow", description = "Test Account DAO: long numbers")
     public void testLongPhoneNumber() throws AccountApiException {
         final AccountModelDao account = createTestAccount("123456789012345678901234");
         accountDao.create(account, internalCallContext);
@@ -90,7 +94,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
     }
 
     // Simple test to ensure excessively long phone numbers cannot be stored
-    @Test(groups = "slow", description="Test Account DAO: very long numbers")
+    @Test(groups = "slow", description = "Test Account DAO: very long numbers")
     public void testOverlyLongPhoneNumber() throws AccountApiException {
         final AccountModelDao account = createTestAccount("12345678901234567890123456");
         try {
@@ -101,7 +105,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         }
     }
 
-    @Test(groups = "slow", description="Test Account DAO: custom fields")
+    @Test(groups = "slow", description = "Test Account DAO: custom fields")
     public void testCustomFields() throws CustomFieldApiException {
         final UUID accountId = UUID.randomUUID();
         final String fieldName = UUID.randomUUID().toString().substring(0, 4);
@@ -118,7 +122,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(customField.getFieldValue(), fieldValue);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: tags")
+    @Test(groups = "slow", description = "Test Account DAO: tags")
     public void testTags() throws TagApiException, TagDefinitionApiException {
         final AccountModelDao account = createTestAccount();
         final TagDefinitionModelDao tagDefinition = tagDefinitionDao.create(UUID.randomUUID().toString().substring(0, 4), UUID.randomUUID().toString(), internalCallContext);
@@ -132,7 +136,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(tags.get(0).getObjectType(), ObjectType.ACCOUNT);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: retrieve by externalKey")
+    @Test(groups = "slow", description = "Test Account DAO: retrieve by externalKey")
     public void testGetIdFromKey() throws AccountApiException {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
@@ -141,12 +145,12 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(accountId, account.getId());
     }
 
-    @Test(groups = "slow", expectedExceptions = AccountApiException.class, description="Test Account DAO: retrieve by null externalKey throws an exception")
+    @Test(groups = "slow", expectedExceptions = AccountApiException.class, description = "Test Account DAO: retrieve by null externalKey throws an exception")
     public void testGetIdFromKeyForNullKey() throws AccountApiException {
         accountDao.getIdFromKey(null, internalCallContext);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: basic update (1)")
+    @Test(groups = "slow", description = "Test Account DAO: basic update (1)")
     public void testUpdate() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
@@ -163,7 +167,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         checkAccountsEqual(retrievedAccount, updatedAccount);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: payment method update")
+    @Test(groups = "slow", description = "Test Account DAO: payment method update")
     public void testUpdatePaymentMethod() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
@@ -181,7 +185,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertNull(newAccountWithPMNull.getPaymentMethodId());
     }
 
-    @Test(groups = "slow", description="Test Account DAO: basic update (2)")
+    @Test(groups = "slow", description = "Test Account DAO: basic update (2)")
     public void testShouldBeAbleToUpdateSomeFields() throws Exception {
         final AccountModelDao account = createTestAccount();
         accountDao.create(account, internalCallContext);
@@ -196,7 +200,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         checkAccountsEqual(retrievedAccount, newAccount);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: BCD of 0")
+    @Test(groups = "slow", description = "Test Account DAO: BCD of 0")
     public void testShouldBeAbleToHandleBCDOfZero() throws Exception {
         final AccountModelDao account = createTestAccount(0);
         accountDao.create(account, internalCallContext);
@@ -206,7 +210,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         checkAccountsEqual(retrievedAccount, account);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: duplicate emails throws an exception")
+    @Test(groups = "slow", description = "Test Account DAO: duplicate emails throws an exception")
     public void testHandleDuplicateEmails() throws AccountApiException {
         final UUID accountId = UUID.randomUUID();
         final AccountEmail email = new DefaultAccountEmail(accountId, "test@gmail.com");
@@ -224,7 +228,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         }
     }
 
-    @Test(groups = "slow", description="Test Account DAO: add and remove email")
+    @Test(groups = "slow", description = "Test Account DAO: add and remove email")
     public void testAddRemoveAccountEmail() throws AccountApiException {
         final UUID accountId = UUID.randomUUID();
 
@@ -247,7 +251,7 @@ public class TestAccountDao extends AccountTestSuiteWithEmbeddedDB {
         Assert.assertEquals(accountDao.getEmailsByAccountId(accountId, internalCallContext).size(), 0);
     }
 
-    @Test(groups = "slow", description="Test Account DAO: add and remove multiple emails")
+    @Test(groups = "slow", description = "Test Account DAO: add and remove multiple emails")
     public void testAddAndRemoveMultipleAccountEmails() throws AccountApiException {
         final UUID accountId = UUID.randomUUID();
         final String email1 = UUID.randomUUID().toString();
diff --git a/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java b/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
index 02ee51e..684ca30 100644
--- a/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
+++ b/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public interface AccountInternalApi {
 
@@ -30,6 +31,8 @@ public interface AccountInternalApi {
 
     public Account getAccountByRecordId(Long recordId, InternalTenantContext context) throws AccountApiException;
 
+    public Pagination<Account> getAccounts(Long offset, Long limit, InternalTenantContext context);
+
     public void updateAccount(String key, AccountData accountData, InternalCallContext context) throws AccountApiException;
 
     public List<AccountEmail> getEmails(UUID accountId, InternalTenantContext context);
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 1615ee0..6d0feb4 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -32,8 +32,11 @@ import com.ning.billing.ErrorCode;
 import com.ning.billing.ObjectType;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountInternalApi;
 import com.ning.billing.bus.api.PersistentBus;
 import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.invoice.InvoiceDispatcher;
 import com.ning.billing.invoice.api.Invoice;
@@ -48,20 +51,20 @@ import com.ning.billing.invoice.model.DefaultInvoice;
 import com.ning.billing.invoice.model.ExternalChargeInvoiceItem;
 import com.ning.billing.invoice.model.InvoiceItemFactory;
 import com.ning.billing.invoice.template.HtmlInvoiceGenerator;
+import com.ning.billing.tag.TagInternalApi;
 import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
-import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.callcontext.TenantContext;
-import com.ning.billing.account.api.AccountInternalApi;
-import com.ning.billing.tag.TagInternalApi;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.Tag;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
 public class DefaultInvoiceUserApi implements InvoiceUserApi {
@@ -111,6 +114,21 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
     }
 
     @Override
+    public Pagination<Invoice> getInvoices(final Long offset, final Long limit, final TenantContext context) {
+        // Invoices will be shallow, i.e. won't contain items nor payments
+        final Pagination<InvoiceModelDao> invoiceModelDaos = dao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+        return new DefaultPagination<Invoice>(invoiceModelDaos,
+                                              limit,
+                                              Iterators.<InvoiceModelDao, Invoice>transform(invoiceModelDaos.iterator(),
+                                                                                            new Function<InvoiceModelDao, Invoice>() {
+                                                                                                @Override
+                                                                                                public Invoice apply(final InvoiceModelDao input) {
+                                                                                                    return new DefaultInvoice(input);
+                                                                                                }
+                                                                                            }));
+    }
+
+    @Override
     public BigDecimal getAccountBalance(final UUID accountId, final TenantContext context) {
         final BigDecimal result = dao.getAccountBalance(accountId, internalCallContextFactory.createInternalTenantContext(accountId, context));
         return result == null ? BigDecimal.ZERO : result;
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
index c0ea740..c4ee207 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/DefaultInvoiceDao.java
@@ -32,6 +32,8 @@ import org.slf4j.LoggerFactory;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.bus.api.PersistentBus;
 import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.clock.Clock;
 import com.ning.billing.invoice.api.Invoice;
@@ -41,8 +43,6 @@ import com.ning.billing.invoice.api.InvoicePaymentType;
 import com.ning.billing.invoice.api.user.DefaultInvoiceAdjustmentEvent;
 import com.ning.billing.invoice.notification.NextBillingDatePoster;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
@@ -149,21 +149,6 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
     }
 
     @Override
-    public List<InvoiceModelDao> get(final InternalTenantContext context) {
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<InvoiceModelDao>>() {
-            @Override
-            public List<InvoiceModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
-                final InvoiceSqlDao invoiceDao = entitySqlDaoWrapperFactory.become(InvoiceSqlDao.class);
-
-                final List<InvoiceModelDao> invoices = invoiceDao.get(context);
-                invoiceDaoHelper.populateChildren(invoices, entitySqlDaoWrapperFactory, context);
-
-                return invoices;
-            }
-        });
-    }
-
-    @Override
     public InvoiceModelDao getById(final UUID invoiceId, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<InvoiceModelDao>() {
             @Override
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
index 4e6170c..8f2c668 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDao.java
@@ -26,22 +26,20 @@ import javax.annotation.Nullable;
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
-import com.ning.billing.catalog.api.Currency;
-import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.util.entity.dao.EntityDao;
 
-public interface InvoiceDao {
+public interface InvoiceDao extends EntityDao<InvoiceModelDao, Invoice, InvoiceApiException> {
 
     void createInvoice(InvoiceModelDao invoice, List<InvoiceItemModelDao> invoiceItems,
                        List<InvoicePaymentModelDao> invoicePayments, boolean isRealInvoice, final Map<UUID, DateTime> callbackDateTimePerSubscriptions, InternalCallContext context);
 
-    InvoiceModelDao getById(UUID id, InternalTenantContext context);
-
     InvoiceModelDao getByNumber(Integer number, InternalTenantContext context) throws InvoiceApiException;
 
-    List<InvoiceModelDao> get(InternalTenantContext context);
-
     List<InvoiceModelDao> getInvoicesByAccount(InternalTenantContext context);
 
     List<InvoiceModelDao> getInvoicesByAccount(LocalDate fromDate, InternalTenantContext context);
@@ -58,8 +56,6 @@ public interface InvoiceDao {
 
     List<InvoiceModelDao> getUnpaidInvoicesByAccountId(UUID accountId, @Nullable LocalDate upToDate, InternalTenantContext context);
 
-    void test(InternalTenantContext context);
-
     // Include migrated invoices
     List<InvoiceModelDao> getAllInvoicesByAccount(InternalTenantContext context);
 
@@ -165,9 +161,8 @@ public interface InvoiceDao {
     void notifyOfPayment(InvoicePaymentModelDao invoicePayment, InternalCallContext context);
 
     /**
-     *
      * @param accountId the account for which we need to rebalance the CBA
-     * @param context the callcontext
+     * @param context   the callcontext
      */
     public void consumeExstingCBAOnAccountWithUnpaidInvoices(final UUID accountId, final InternalCallContext context);
 }
diff --git a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDaoHelper.java b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDaoHelper.java
index e34563f..9d07999 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDaoHelper.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/dao/InvoiceDaoHelper.java
@@ -28,12 +28,12 @@ import javax.annotation.Nullable;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.ErrorCode;
-import com.ning.billing.catalog.api.Currency;
-import com.ning.billing.invoice.api.InvoiceApiException;
-import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.entity.EntityPersistenceException;
+import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceItemType;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
 
@@ -223,7 +223,6 @@ public class InvoiceDaoHelper {
         transInvoiceItemDao.create(item, context);
     }
 
-
     public void populateChildren(final InvoiceModelDao invoice, final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory, final InternalTenantContext context) {
         getInvoiceItemsWithinTransaction(invoice, entitySqlDaoWrapperFactory, context);
         getInvoicePaymentsWithinTransaction(invoice, entitySqlDaoWrapperFactory, context);
@@ -271,6 +270,4 @@ public class InvoiceDaoHelper {
         final List<InvoicePaymentModelDao> invoicePayments = invoicePaymentSqlDao.getPaymentsForInvoice(invoiceId, context);
         invoice.addPayments(invoicePayments);
     }
-
-
 }
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
index a36cf1e..5006858 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockInvoiceDao.java
@@ -30,17 +30,21 @@ import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 
 import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
 import com.ning.billing.invoice.api.user.DefaultInvoiceCreationEvent;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.MockEntityDaoBase;
 
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.inject.Inject;
 
-public class MockInvoiceDao implements InvoiceDao {
+public class MockInvoiceDao extends MockEntityDaoBase<InvoiceModelDao, Invoice, InvoiceApiException> implements InvoiceDao {
 
     private final PersistentBus eventBus;
     private final Object monitor = new Object();
@@ -70,7 +74,7 @@ public class MockInvoiceDao implements InvoiceDao {
         try {
             eventBus.post(new DefaultInvoiceCreationEvent(invoice.getId(), invoice.getAccountId(),
                                                           InvoiceModelDaoHelper.getBalance(invoice), invoice.getCurrency(),
-                          context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
+                                                          context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()));
         } catch (PersistentBus.EventBusException ex) {
             throw new RuntimeException(ex);
         }
@@ -97,9 +101,9 @@ public class MockInvoiceDao implements InvoiceDao {
     }
 
     @Override
-    public List<InvoiceModelDao> get(final InternalTenantContext context) {
+    public Pagination<InvoiceModelDao> getAll(final InternalTenantContext context) {
         synchronized (monitor) {
-            return new ArrayList<InvoiceModelDao>(invoices.values());
+            return new DefaultPagination<InvoiceModelDao>((long) invoices.values().size(), invoices.values().iterator());
         }
     }
 
@@ -124,7 +128,7 @@ public class MockInvoiceDao implements InvoiceDao {
 
         synchronized (monitor) {
             final UUID accountId = accountRecordIds.inverse().get(context.getAccountRecordId());
-            for (final InvoiceModelDao invoice : get(context)) {
+            for (final InvoiceModelDao invoice : getAll(context)) {
                 if (accountId.equals(invoice.getAccountId()) && !invoice.getTargetDate().isBefore(fromDate) && !invoice.isMigrated()) {
                     invoicesForAccount.add(invoice);
                 }
@@ -195,7 +199,7 @@ public class MockInvoiceDao implements InvoiceDao {
     public BigDecimal getAccountBalance(final UUID accountId, final InternalTenantContext context) {
         BigDecimal balance = BigDecimal.ZERO;
 
-        for (final InvoiceModelDao invoice : get(context)) {
+        for (final InvoiceModelDao invoice : getAll(context)) {
             if (accountId.equals(invoice.getAccountId())) {
                 balance = balance.add(InvoiceModelDaoHelper.getBalance(invoice));
             }
@@ -208,7 +212,7 @@ public class MockInvoiceDao implements InvoiceDao {
     public List<InvoiceModelDao> getUnpaidInvoicesByAccountId(final UUID accountId, final LocalDate upToDate, final InternalTenantContext context) {
         final List<InvoiceModelDao> unpaidInvoices = new ArrayList<InvoiceModelDao>();
 
-        for (final InvoiceModelDao invoice : get(context)) {
+        for (final InvoiceModelDao invoice : getAll(context)) {
             if (accountId.equals(invoice.getAccountId()) && (InvoiceModelDaoHelper.getBalance(invoice).compareTo(BigDecimal.ZERO) > 0) && !invoice.isMigrated()) {
                 unpaidInvoices.add(invoice);
             }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index 95eed17..a210318 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -16,7 +16,10 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.math.BigDecimal;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -36,8 +39,10 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
 import javax.ws.rs.core.UriInfo;
 
 import com.ning.billing.ErrorCode;
@@ -92,12 +97,14 @@ import com.ning.billing.util.audit.AuditLogsForPayments;
 import com.ning.billing.util.audit.AuditLogsForRefunds;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.tag.ControlTagType;
 
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Function;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Multimap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -151,24 +158,61 @@ public class AccountResource extends JaxRsResourceBase {
     }
 
     @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getAccounts(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                @QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
+                                @QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
+                                @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<Account> accounts = accountUserApi.getAccounts(offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "getAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of());
+        return buildStreamingAccountsResponse(accounts, accountWithBalance, accountWithBalanceAndCBA, nextPageUri, tenantContext);
+    }
+
+    @GET
     @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
     @Produces(APPLICATION_JSON)
     public Response searchAccounts(@PathParam("searchKey") final String searchKey,
+                                   @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                   @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
                                    @QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
                                    @QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
                                    @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
         final TenantContext tenantContext = context.createContext(request);
-        final List<Account> accounts = accountUserApi.searchAccounts(searchKey, tenantContext);
-        final List<AccountJson> accountsJson = ImmutableList.<AccountJson>copyOf(Collections2.transform(accounts, new Function<Account, AccountJson>() {
+        final Pagination<Account> accounts = accountUserApi.searchAccounts(searchKey, offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "searchAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey));
+        return buildStreamingAccountsResponse(accounts, accountWithBalance, accountWithBalanceAndCBA, nextPageUri, tenantContext);
+    }
+
+    private Response buildStreamingAccountsResponse(final Pagination<Account> accounts, final Boolean accountWithBalance,
+                                                    final Boolean accountWithBalanceAndCBA, final URI nextPageUri, final TenantContext tenantContext) {
+        final StreamingOutput json = new StreamingOutput() {
             @Override
-            public AccountJson apply(final Account account) {
-                return getAccount(account, accountWithBalance, accountWithBalanceAndCBA, tenantContext);
+            public void write(final OutputStream output) throws IOException, WebApplicationException {
+                final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+                generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+                generator.writeStartArray();
+                for (final Account account : accounts) {
+                    final AccountJson asJson = getAccount(account, accountWithBalance, accountWithBalanceAndCBA, tenantContext);
+                    generator.writeObject(asJson);
+                }
+                generator.writeEndArray();
+                generator.close();
             }
-        }));
-        return Response.status(Status.OK).entity(accountsJson).build();
+        };
+        return Response.status(Status.OK)
+                       .entity(json)
+                       .header(HDR_PAGINATION_CURRENT_OFFSET, accounts.getCurrentOffset())
+                       .header(HDR_PAGINATION_NEXT_OFFSET, accounts.getNextOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, accounts.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, accounts.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+                       .build();
     }
 
-
     @GET
     @Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES)
     @Produces(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 8f91ef2..4d73b69 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -40,6 +40,11 @@ public interface JaxrsResource {
     public static String HDR_CREATED_BY = "X-Killbill-CreatedBy";
     public static String HDR_REASON = "X-Killbill-Reason";
     public static String HDR_COMMENT = "X-Killbill-Comment";
+    public static String HDR_PAGINATION_CURRENT_OFFSET = "X-Killbill-Pagination-CurrentOffset";
+    public static String HDR_PAGINATION_NEXT_OFFSET = "X-Killbill-Pagination-NextOffset";
+    public static String HDR_PAGINATION_TOTAL_NB_RECORDS = "X-Killbill-Pagination-TotalNbRecords";
+    public static String HDR_PAGINATION_MAX_NB_RECORDS = "X-Killbill-Pagination-MaxNbRecords";
+    public static String HDR_PAGINATION_NEXT_PAGE_URI = "X-Killbill-Pagination-NextPageUri";
 
     /*
      * Patterns
@@ -62,6 +67,8 @@ public interface JaxrsResource {
     public static final String QUERY_TARGET_DATE = "targetDate";
     public static final String QUERY_BILLING_POLICY = "billingPolicy";
     public static final String QUERY_ENTITLEMENT_POLICY = "entitlementPolicy";
+    public static final String QUERY_SEARCH_OFFSET = "offset";
+    public static final String QUERY_SEARCH_LIMIT = "limit";
 
     public static final String QUERY_ACCOUNT_WITH_BALANCE = "accountWithBalance";
     public static final String QUERY_ACCOUNT_WITH_BALANCE_AND_CBA = "accountWithBalanceAndCBA";
@@ -93,6 +100,8 @@ public interface JaxrsResource {
 
     public static final String QUERY_NOTIFICATION_CALLBACK = "cb";
 
+    public static final String PAGINATION = "pagination";
+
     public static final String ACCOUNTS = "accounts";
     public static final String ACCOUNTS_PATH = PREFIX + "/" + ACCOUNTS;
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
index 3915e39..c24c399 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -53,6 +53,7 @@ import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.customfield.StringCustomField;
+import com.ning.billing.util.jackson.ObjectMapper;
 import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.TagDefinition;
 
@@ -64,6 +65,8 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
 
     private static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
 
+    protected static final ObjectMapper mapper = new ObjectMapper();
+
     protected final JaxrsUriBuilder uriBuilder;
     protected final TagUserApi tagUserApi;
     protected final CustomFieldUserApi customFieldUserApi;
@@ -172,7 +175,7 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
 
     protected LocalDate toLocalDate(final UUID accountId, final String inputDate, final TenantContext context) {
 
-        final LocalDate maybeResult  = extractLocalDate(inputDate);
+        final LocalDate maybeResult = extractLocalDate(inputDate);
         if (maybeResult != null) {
             return maybeResult;
         }
@@ -187,10 +190,9 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
     }
 
 
-
     protected LocalDate toLocalDate(final Account account, final String inputDate, final TenantContext context) {
 
-        final LocalDate maybeResult  = extractLocalDate(inputDate);
+        final LocalDate maybeResult = extractLocalDate(inputDate);
         if (maybeResult != null) {
             return maybeResult;
         }
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
index 1218a08..1cdc630 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
@@ -16,8 +16,10 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -30,8 +32,10 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
 
 import com.ning.billing.ObjectType;
 import com.ning.billing.account.api.Account;
@@ -49,10 +53,11 @@ import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
-import com.google.common.base.Function;
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -93,37 +98,85 @@ public class PaymentMethodResource extends JaxRsResourceBase {
     }
 
     @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getPaymentMethods(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                      @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                      @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_NAME) final String pluginName,
+                                      @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        final Pagination<PaymentMethod> paymentMethods;
+        final Map<String, String> nextUriParams = new HashMap<String, String>();
+        if (Strings.isNullOrEmpty(pluginName)) {
+            paymentMethods = paymentApi.getPaymentMethods(offset, limit, tenantContext);
+        } else {
+            paymentMethods = paymentApi.getPaymentMethods(offset, limit, pluginName, tenantContext);
+            nextUriParams.put(QUERY_PAYMENT_METHOD_PLUGIN_NAME, pluginName);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "getPaymentMethods", paymentMethods.getNextOffset(), limit, nextUriParams);
+        return buildStreamingPaymentMethodsResponse(paymentMethods, nextPageUri, tenantContext);
+    }
+
+    @GET
     @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
     @Produces(APPLICATION_JSON)
     public Response searchPaymentMethods(@PathParam("searchKey") final String searchKey,
+                                         @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                         @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
                                          @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_NAME) final String pluginName,
                                          @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
         final TenantContext tenantContext = context.createContext(request);
 
         // Search the plugin(s)
-        final List<PaymentMethod> paymentMethods;
+        final Pagination<PaymentMethod> paymentMethods;
         if (Strings.isNullOrEmpty(pluginName)) {
-            paymentMethods = paymentApi.searchPaymentMethods(searchKey, tenantContext);
+            paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, tenantContext);
         } else {
-            paymentMethods = paymentApi.searchPaymentMethods(searchKey, pluginName, tenantContext);
+            paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, pluginName, tenantContext);
         }
 
-        // Lookup the associated account(s)
-        final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
-        for (final PaymentMethod paymentMethod : paymentMethods) {
-            if (accounts.get(paymentMethod.getAccountId()) == null) {
-                final Account account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
-                accounts.put(paymentMethod.getAccountId(), account);
-            }
-        }
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "searchPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of());
+        return buildStreamingPaymentMethodsResponse(paymentMethods, nextPageUri, tenantContext);
+    }
 
-        final List<PaymentMethodJson> json = Lists.transform(paymentMethods, new Function<PaymentMethod, PaymentMethodJson>() {
+    private Response buildStreamingPaymentMethodsResponse(final Pagination<PaymentMethod> paymentMethods, final URI nextPageUri, final TenantContext tenantContext) {
+        final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
+        final StreamingOutput json = new StreamingOutput() {
             @Override
-            public PaymentMethodJson apply(final PaymentMethod paymentMethod) {
-                return PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod);
+            public void write(final OutputStream output) throws IOException, WebApplicationException {
+                final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+                generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+                generator.writeStartArray();
+                for (final PaymentMethod paymentMethod : paymentMethods) {
+                    // Lookup the associated account(s)
+                    if (accounts.get(paymentMethod.getAccountId()) == null) {
+                        final Account account;
+                        try {
+                            account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+                            accounts.put(paymentMethod.getAccountId(), account);
+                        } catch (AccountApiException e) {
+                            throw new RuntimeException(e);
+                        }
+                    }
+
+                    final PaymentMethodJson asJson = PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod);
+                    generator.writeObject(asJson);
+                }
+                generator.writeEndArray();
+                generator.close();
             }
-        });
-        return Response.status(Status.OK).entity(json).build();
+        };
+        return Response.status(Status.OK)
+                       .entity(json)
+                       .header(HDR_PAGINATION_CURRENT_OFFSET, paymentMethods.getCurrentOffset())
+                       .header(HDR_PAGINATION_NEXT_OFFSET, paymentMethods.getNextOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, paymentMethods.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, paymentMethods.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+                       .build();
     }
 
     @DELETE
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
index d7ca875..4a25ec0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -13,12 +13,16 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.jaxrs.util;
 
+import java.net.URI;
+import java.util.Map;
+
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
 
+import com.ning.billing.jaxrs.resources.JaxRsResourceBase;
 import com.ning.billing.jaxrs.resources.JaxrsResource;
 
 public class JaxrsUriBuilder {
@@ -37,6 +41,21 @@ public class JaxrsUriBuilder {
         }).build();
     }
 
+    public URI nextPage(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Long nextOffset, final Long limit, final Map<String, String> params) {
+        if (nextOffset == null || limit == null) {
+            // End of pagination?
+            return null;
+        }
+
+        final UriBuilder uriBuilder = UriBuilder.fromResource(theClass)
+                                                .path(theClass, getMethodName)
+                                                .queryParam(JaxRsResourceBase.QUERY_SEARCH_OFFSET, nextOffset)
+                                                .queryParam(JaxRsResourceBase.QUERY_SEARCH_LIMIT, limit);
+        for (final String key : params.keySet()) {
+            uriBuilder.queryParam(key, params.get(key));
+        }
+        return uriBuilder.build();
+    }
 
     public Response buildResponse(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId, final String baseUri) {
 
diff --git a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
index c318727..88066fa 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
@@ -187,6 +187,7 @@ public class FileInstall {
         if (new File(expectedPath).isFile()) {
             return expectedPath;
         } else {
+            logger.warn("Unable to find the JRuby bundle at {}, ruby plugins won't be started!", expectedPath);
             return null;
         }
     }
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
index 4868c1f..f3ab64c 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -38,6 +38,7 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi {
 
@@ -168,11 +169,11 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
         return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
             @Override
-            public List<PaymentMethodPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
-                return ((PaymentPluginApi) pluginInstance).searchPaymentMethods(searchKey, tenantContext);
+            public Pagination<PaymentMethodPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+                return ((PaymentPluginApi) pluginInstance).searchPaymentMethods(searchKey, offset, limit, tenantContext);
             }
         });
     }
diff --git a/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
index 600f5af..c4ef01b 100644
--- a/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -18,6 +18,7 @@ package com.ning.billing.osgi.bundles.test;
 
 import java.math.BigDecimal;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -34,6 +35,7 @@ import com.ning.billing.payment.plugin.api.PaymentPluginStatus;
 import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public class TestPaymentPluginApi implements PaymentPluginApi {
 
@@ -129,8 +131,33 @@ public class TestPaymentPluginApi implements PaymentPluginApi {
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return Collections.emptyList();
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new Pagination<PaymentMethodPlugin>() {
+            @Override
+            public Long getCurrentOffset() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNextOffset() {
+                return null;
+            }
+
+            @Override
+            public Long getMaxNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Long getTotalNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Iterator<PaymentMethodPlugin> iterator() {
+                return null;
+            }
+        };
     }
 
     @Override
diff --git a/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
index 804e548..c9059b0 100644
--- a/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -18,6 +18,7 @@ package com.ning.billing.osgi.bundles.test;
 
 import java.math.BigDecimal;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -34,6 +35,7 @@ import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
 
@@ -54,22 +56,27 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
             public BigDecimal getAmount() {
                 return amount;
             }
+
             @Override
             public DateTime getCreatedDate() {
                 return new DateTime();
             }
+
             @Override
             public DateTime getEffectiveDate() {
                 return new DateTime();
             }
+
             @Override
             public PaymentPluginStatus getStatus() {
                 return PaymentPluginStatus.PROCESSED;
             }
+
             @Override
             public String getGatewayError() {
                 return null;
             }
+
             @Override
             public String getGatewayErrorCode() {
                 return null;
@@ -96,22 +103,27 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
             public BigDecimal getAmount() {
                 return someAmount;
             }
+
             @Override
             public DateTime getCreatedDate() {
                 return new DateTime();
             }
+
             @Override
             public DateTime getEffectiveDate() {
                 return new DateTime();
             }
+
             @Override
             public PaymentPluginStatus getStatus() {
                 return PaymentPluginStatus.PROCESSED;
             }
+
             @Override
             public String getGatewayError() {
                 return null;
             }
+
             @Override
             public String getGatewayErrorCode() {
                 return null;
@@ -138,22 +150,27 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
             public BigDecimal getAmount() {
                 return null;
             }
+
             @Override
             public DateTime getCreatedDate() {
                 return null;
             }
+
             @Override
             public DateTime getEffectiveDate() {
                 return null;
             }
+
             @Override
             public RefundPluginStatus getStatus() {
                 return null;
             }
+
             @Override
             public String getGatewayError() {
                 return null;
             }
+
             @Override
             public String getGatewayErrorCode() {
                 return null;
@@ -194,8 +211,33 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String s, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return Collections.emptyList();
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new Pagination<PaymentMethodPlugin>() {
+            @Override
+            public Long getCurrentOffset() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNextOffset() {
+                return null;
+            }
+
+            @Override
+            public Long getMaxNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Long getTotalNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Iterator<PaymentMethodPlugin> iterator() {
+                return null;
+            }
+        };
     }
 
     @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index d0f7b65..579ded0 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -26,13 +26,18 @@ import java.util.UUID;
 
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
+import com.ning.billing.callcontext.DefaultCallContext;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.clock.Clock;
 import com.ning.billing.payment.core.PaymentMethodProcessor;
 import com.ning.billing.payment.core.PaymentProcessor;
 import com.ning.billing.payment.core.RefundProcessor;
 import com.ning.billing.util.callcontext.CallContext;
-import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.callcontext.UserType;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
@@ -43,15 +48,18 @@ public class DefaultPaymentApi implements PaymentApi {
     private final PaymentProcessor paymentProcessor;
     private final RefundProcessor refundProcessor;
     private final InternalCallContextFactory internalCallContextFactory;
+    private final Clock clock;
 
     @Inject
     public DefaultPaymentApi(final PaymentMethodProcessor methodProcessor,
                              final PaymentProcessor paymentProcessor,
                              final RefundProcessor refundProcessor,
+                             final Clock clock,
                              final InternalCallContextFactory internalCallContextFactory) {
         this.methodProcessor = methodProcessor;
         this.paymentProcessor = paymentProcessor;
         this.refundProcessor = refundProcessor;
+        this.clock = clock;
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
@@ -173,13 +181,27 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
-    public List<PaymentMethod> searchPaymentMethods(final String searchKey, final TenantContext context) {
-        return methodProcessor.searchPaymentMethods(searchKey, internalCallContextFactory.createInternalTenantContext(context));
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final TenantContext context) {
+        // Lame, but required
+        final CallContext callContext = new DefaultCallContext(context.getTenantId(), DefaultPaymentApi.class.toString(), CallOrigin.EXTERNAL, UserType.CUSTOMER, UUID.randomUUID(), clock);
+        return methodProcessor.getPaymentMethods(offset, limit, context, callContext, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+        // Lame, but required
+        final CallContext callContext = new DefaultCallContext(context.getTenantId(), DefaultPaymentApi.class.toString(), CallOrigin.EXTERNAL, UserType.CUSTOMER, UUID.randomUUID(), clock);
+        return methodProcessor.getPaymentMethods(offset, limit, pluginName, context, callContext, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+        return methodProcessor.searchPaymentMethods(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
     }
 
     @Override
-    public List<PaymentMethod> searchPaymentMethods(final String searchKey, final String pluginName, final TenantContext context) throws PaymentApiException {
-        return methodProcessor.searchPaymentMethods(searchKey, pluginName, internalCallContextFactory.createInternalTenantContext(context));
+    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+        return methodProcessor.searchPaymentMethods(searchKey, offset, limit, pluginName, internalCallContextFactory.createInternalTenantContext(context));
     }
 
     @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
index cd50517..f720133 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
@@ -32,8 +32,12 @@ import org.slf4j.LoggerFactory;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
+import com.ning.billing.account.api.AccountInternalApi;
 import com.ning.billing.bus.api.PersistentBus;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.commons.locker.GlobalLocker;
+import com.ning.billing.invoice.api.InvoiceInternalApi;
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
 import com.ning.billing.payment.api.DefaultPaymentMethod;
 import com.ning.billing.payment.api.PaymentApiException;
@@ -48,16 +52,18 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 import com.ning.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
 import com.ning.billing.payment.provider.DefaultPaymentMethodInfoPlugin;
 import com.ning.billing.payment.provider.ExternalPaymentProviderPlugin;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
-import com.ning.billing.util.dao.NonEntityDao;
-import com.ning.billing.account.api.AccountInternalApi;
-import com.ning.billing.invoice.api.InvoiceInternalApi;
 import com.ning.billing.tag.TagInternalApi;
+import com.ning.billing.util.callcontext.CallContext;
+import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Function;
+import com.google.common.base.Predicates;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
 
@@ -153,49 +159,128 @@ public class PaymentMethodProcessor extends ProcessorBase {
         return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
     }
 
-    public List<PaymentMethod> searchPaymentMethods(final String searchKey, final InternalTenantContext internalTenantContext) {
-        final List<PaymentMethod> results = new LinkedList<PaymentMethod>();
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final TenantContext tenantContext, final CallContext callContext, final InternalTenantContext internalTenantContext) {
+        // Note that we cannot easily do streaming here, since we would have to rely on the statistics
+        // returned by the Pagination objects from the plugins and we probably don't want to do that (if
+        // one plugin gets it wrong, it may starve the others).
+        final List<PaymentMethod> allResults = new LinkedList<PaymentMethod>();
 
-        // Search in all plugins
+        // Search in all plugins (we treat the full set of results as a union with respect to offset/limit)
         for (final String pluginName : getAvailablePlugins()) {
             try {
-                results.addAll(searchPaymentMethods(searchKey, pluginName, internalTenantContext));
+                final Pagination<PaymentMethod> paymentMethods = getPaymentMethods(0L, Long.MAX_VALUE, pluginName, tenantContext, callContext, internalTenantContext);
+                allResults.addAll(ImmutableList.<PaymentMethod>copyOf(paymentMethods));
+                if (allResults.size() > offset + limit) {
+                    break;
+                }
             } catch (PaymentApiException e) {
                 log.warn("Error while searching plugin " + pluginName, e);
                 // Non-fatal, continue to search other plugins
             }
         }
 
-        return results;
+        return DefaultPagination.<PaymentMethod>build(offset, limit, allResults);
     }
 
-    public List<PaymentMethod> searchPaymentMethods(final String searchKey, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final CallContext callContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-        final List<PaymentMethodPlugin> paymentMethods;
-        try {
-            paymentMethods = pluginApi.searchPaymentMethods(searchKey, buildTenantContext(internalTenantContext));
-        } catch (PaymentPluginApiException e) {
-            throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
-        }
+        final List<PaymentMethodPlugin> paymentMethods = new LinkedList<PaymentMethodPlugin>();
 
-        final List<PaymentMethod> results = new LinkedList<PaymentMethod>();
-        for (final PaymentMethodPlugin paymentMethodPlugin : paymentMethods) {
-            if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
-                // Garbage from the plugin?
-                log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
-                continue;
+        // Find all payment methods for all accounts
+        final Pagination<Account> accounts = accountInternalApi.getAccounts(0L, Long.MAX_VALUE, internalTenantContext);
+        for (final Account account : accounts) {
+            try {
+                final List<PaymentMethodInfoPlugin> paymentMethodInfos = pluginApi.getPaymentMethods(account.getId(), true, callContext);
+                for (final PaymentMethodInfoPlugin paymentMethodInfoPlugin : paymentMethodInfos) {
+                    final PaymentMethodPlugin paymentMethodDetail = pluginApi.getPaymentMethodDetail(account.getId(), paymentMethodInfoPlugin.getPaymentMethodId(), tenantContext);
+                    paymentMethods.add(paymentMethodDetail);
+                    if (paymentMethods.size() > offset + limit) {
+                        break;
+                    }
+                }
+            } catch (PaymentPluginApiException e) {
+                throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_GET_PAYMENT_METHODS, pluginName);
             }
+        }
+
+        return new DefaultPagination<PaymentMethod>(DefaultPagination.<PaymentMethodPlugin>build(offset, limit, paymentMethods),
+                                                    limit,
+                                                    Iterators.<PaymentMethod>filter(Iterators.<PaymentMethodPlugin, PaymentMethod>transform(paymentMethods.iterator(),
+                                                                                                                                            new Function<PaymentMethodPlugin, PaymentMethod>() {
+                                                                                                                                                @Override
+                                                                                                                                                public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
+                                                                                                                                                    if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+                                                                                                                                                        // Garbage from the plugin?
+                                                                                                                                                        log.debug("Plugin {} returned a payment method without a kbPaymentMethodId", pluginName);
+                                                                                                                                                        return null;
+                                                                                                                                                    }
+
+                                                                                                                                                    final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
+                                                                                                                                                    if (paymentMethodModelDao == null) {
+                                                                                                                                                        log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
+                                                                                                                                                        return null;
+                                                                                                                                                    }
+
+                                                                                                                                                    return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+                                                                                                                                                }
+                                                                                                                                            }),
+                                                                                    Predicates.<PaymentMethod>notNull()));
+    }
+
+    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
+        // Note that we cannot easily do streaming here, since we would have to rely on the statistics
+        // returned by the Pagination objects from the plugins and we probably don't want to do that (if
+        // one plugin gets it wrong, it may starve the others).
+        final List<PaymentMethod> allResults = new LinkedList<PaymentMethod>();
 
-            final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
-            if (paymentMethodModelDao == null) {
-                log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
-                continue;
+        // Search in all plugins (we treat the full set of results as a union with respect to offset/limit)
+        for (final String pluginName : getAvailablePlugins()) {
+            try {
+                final Pagination<PaymentMethod> paymentMethods = searchPaymentMethods(searchKey, 0L, Long.MAX_VALUE, pluginName, internalTenantContext);
+                allResults.addAll(ImmutableList.<PaymentMethod>copyOf(paymentMethods));
+                if (allResults.size() > offset + limit) {
+                    break;
+                }
+            } catch (PaymentApiException e) {
+                log.warn("Error while searching plugin " + pluginName, e);
+                // Non-fatal, continue to search other plugins
             }
+        }
+
+        return DefaultPagination.<PaymentMethod>build(offset, limit, allResults);
+    }
 
-            results.add(new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin));
+    public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+        final Pagination<PaymentMethodPlugin> paymentMethods;
+        try {
+            paymentMethods = pluginApi.searchPaymentMethods(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+        } catch (PaymentPluginApiException e) {
+            throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
         }
 
-        return results;
+        return new DefaultPagination<PaymentMethod>(paymentMethods,
+                                                    limit,
+                                                    Iterators.<PaymentMethod>filter(Iterators.<PaymentMethodPlugin, PaymentMethod>transform(paymentMethods.iterator(),
+                                                                                                                                            new Function<PaymentMethodPlugin, PaymentMethod>() {
+                                                                                                                                                @Override
+                                                                                                                                                public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
+                                                                                                                                                    if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+                                                                                                                                                        // Garbage from the plugin?
+                                                                                                                                                        log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
+                                                                                                                                                        return null;
+                                                                                                                                                    }
+
+                                                                                                                                                    final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
+                                                                                                                                                    if (paymentMethodModelDao == null) {
+                                                                                                                                                        log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
+                                                                                                                                                        return null;
+                                                                                                                                                    }
+
+                                                                                                                                                    return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+                                                                                                                                                }
+                                                                                                                                            }),
+                                                                                    Predicates.<PaymentMethod>notNull()));
     }
 
     public PaymentMethod getExternalPaymentMethod(final Account account, final InternalTenantContext context) throws PaymentApiException {
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
index 449324a..f934d59 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
@@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.clock.Clock;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
 import com.ning.billing.payment.plugin.api.NoOpPaymentPluginApi;
 import com.ning.billing.payment.plugin.api.PaymentInfoPlugin;
@@ -35,7 +36,8 @@ import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
-import com.ning.billing.clock.Clock;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
@@ -157,8 +159,8 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(Iterables.<PaymentMethodPlugin>concat(paymentMethods.values()), new Predicate<PaymentMethodPlugin>() {
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        final ImmutableList<PaymentMethodPlugin> results = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(Iterables.<PaymentMethodPlugin>concat(paymentMethods.values()), new Predicate<PaymentMethodPlugin>() {
             @Override
             public boolean apply(final PaymentMethodPlugin input) {
                 return (input.getAddress1() != null && input.getAddress1().contains(searchKey)) ||
@@ -170,6 +172,7 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
                        (input.getCountry() != null && input.getCountry().contains(searchKey));
             }
         }));
+        return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, results);
     }
 
     @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
index b1b6915..1605150 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
@@ -34,8 +34,11 @@ import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
 /**
@@ -98,8 +101,8 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String s, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return ImmutableList.<PaymentMethodPlugin>of();
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new DefaultPagination<PaymentMethodPlugin>(offset, limit, 0L, 0L, Iterators.<PaymentMethodPlugin>emptyIterator());
     }
 
     @Override
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
index 3768b18..0d383c6 100644
--- a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -37,6 +37,8 @@ import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
@@ -146,8 +148,8 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
     }
 
     @Override
-    public List<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(paymentMethods.values(), new Predicate<PaymentMethodPlugin>() {
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        final ImmutableList<PaymentMethodPlugin> results = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(paymentMethods.values(), new Predicate<PaymentMethodPlugin>() {
             @Override
             public boolean apply(final PaymentMethodPlugin input) {
                 return (input.getAddress1() != null && input.getAddress1().contains(searchKey)) ||
@@ -159,6 +161,7 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
                        (input.getCountry() != null && input.getCountry().contains(searchKey));
             }
         }));
+        return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, results);
     }
 
     @Override

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 274a9b5..1f51d19 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>com.ning.billing</groupId>
-        <version>0.4.18</version>
+        <version>0.5.1</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.6.18-SNAPSHOT</version>
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
index 9e09c6c..8989b1b 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
@@ -16,13 +16,13 @@
 
 package com.ning.billing.util.entity.dao;
 
-import java.util.List;
 import java.util.UUID;
 
 import com.ning.billing.BillingExceptionBase;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
 
 public interface EntityDao<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> {
 
@@ -34,7 +34,11 @@ public interface EntityDao<M extends EntityModelDao<E>, E extends Entity, U exte
 
     public M getById(UUID id, InternalTenantContext context);
 
-    public List<M> get(InternalTenantContext context);
+    public Pagination<M> getAll(InternalTenantContext context);
+
+    public Pagination<M> get(Long offset, Long limit, InternalTenantContext context);
+
+    public Long getCount(InternalTenantContext context);
 
     public void test(InternalTenantContext context);
 }
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
index 1c2704c..d494c4e 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
@@ -16,14 +16,16 @@
 
 package com.ning.billing.util.entity.dao;
 
-import java.util.List;
+import java.util.Iterator;
 import java.util.UUID;
 
 import com.ning.billing.BillingExceptionBase;
-import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.audit.ChangeType;
+import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
 
 public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> implements EntityDao<M, E, U> {
 
@@ -71,6 +73,10 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
 
     protected abstract U generateAlreadyExistsException(final M entity, final InternalCallContext context);
 
+    protected String getNaturalOrderingColumns() {
+        return "recordId";
+    }
+
     @Override
     public Long getRecordId(final UUID id, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
@@ -108,13 +114,56 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
     }
 
     @Override
-    public List<M> get(final InternalTenantContext context) {
-        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<List<M>>() {
+    public Pagination<M> getAll(final InternalTenantContext context) {
+        // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+        // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+        final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(realSqlDao);
+
+        // Note: we need to perform the count before streaming the results, as the connection
+        // will be busy as we stream the results out. This is also why we cannot use
+        // SQL_CALC_FOUND_ROWS / FOUND_ROWS (which may ne be faster anyways).
+        final Long count = sqlDao.getCount(context);
+
+        final Iterator<M> results = sqlDao.getAll(context);
+        return new DefaultPagination<M>(count, results);
+    }
 
+    @Override
+    public Pagination<M> get(final Long offset, final Long limit, final InternalTenantContext context) {
+        // Note: the connection will be busy as we stream the results out: hence we cannot use
+        // SQL_CALC_FOUND_ROWS / FOUND_ROWS on the actual query.
+        // We still need to know the actual number of results, mainly for the UI so that it knows if it needs to fetch
+        // more pages. To do that, we perform a dummy search query with SQL_CALC_FOUND_ROWS (but limit 1).
+        final Long count = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
             @Override
-            public List<M> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+            public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+                final EntitySqlDao<M, E> sqlDao = entitySqlDaoWrapperFactory.become(realSqlDao);
+                final Iterator<M> dumbIterator = sqlDao.get(offset, 1L, getNaturalOrderingColumns(), context);
+                // Make sure to go through the results to close the connection
+                while (dumbIterator.hasNext()) {
+                    dumbIterator.next();
+                }
+                return sqlDao.getFoundRows(context);
+            }
+        });
+
+        // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+        // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+        final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(realSqlDao);
+        final Long totalCount = sqlDao.getCount(context);
+        final Iterator<M> results = sqlDao.get(offset, limit, getNaturalOrderingColumns(), context);
+
+        return new DefaultPagination<M>(offset, limit, count, totalCount, results);
+    }
+
+    @Override
+    public Long getCount(final InternalTenantContext context) {
+        return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+
+            @Override
+            public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
                 final EntitySqlDao<M, E> transactional = entitySqlDaoWrapperFactory.become(realSqlDao);
-                return transactional.get(context);
+                return transactional.getCount(context);
             }
         });
     }
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
index 662cca8..f4c9148 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
@@ -16,26 +16,28 @@
 
 package com.ning.billing.util.entity.dao;
 
+import java.util.Iterator;
 import java.util.List;
 
 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.FetchSize;
 import org.skife.jdbi.v2.sqlobject.mixins.CloseMe;
 import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
 import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
 
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.entity.EntityPersistenceException;
 import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.cache.Cachable;
 import com.ning.billing.util.cache.Cachable.CacheType;
 import com.ning.billing.util.cache.CachableKey;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.dao.AuditSqlDao;
 import com.ning.billing.util.dao.HistorySqlDao;
 import com.ning.billing.util.entity.Entity;
-import com.ning.billing.entity.EntityPersistenceException;
 
 // TODO get rid of Transmogrifier, but code does not compile even if we create the
 // method  public <T> T become(Class<T> typeToBecome); ?
@@ -66,10 +68,26 @@ public interface EntitySqlDao<M extends EntityModelDao<E>, E extends Entity> ext
                             @BindBean final InternalTenantContext context);
 
     @SqlQuery
-    public List<M> get(@BindBean final InternalTenantContext context);
+    public Long getFoundRows(@BindBean final InternalTenantContext context);
 
-    @SqlUpdate
-    public void test(@BindBean final InternalTenantContext context);
+    @SqlQuery
+    // Magic value to force MySQL to stream from the database
+    // See http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html (ResultSet)
+    @FetchSize(Integer.MIN_VALUE)
+    public Iterator<M> getAll(@BindBean final InternalTenantContext context);
 
+    @SqlQuery
+    // Magic value to force MySQL to stream from the database
+    // See http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html (ResultSet)
+    @FetchSize(Integer.MIN_VALUE)
+    public Iterator<M> get(@Bind("offset") final Long offset,
+                           @Bind("rowCount") final Long rowCount,
+                           @Bind("orderBy") final String orderBy,
+                           @BindBean final InternalTenantContext context);
 
+    @SqlQuery
+    public Long getCount(@BindBean final InternalTenantContext context);
+
+    @SqlUpdate
+    public void test(@BindBean final InternalTenantContext context);
 }
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
index db23b1f..db6160f 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDaoTransactionalJdbiWrapper.java
@@ -21,8 +21,8 @@ import org.skife.jdbi.v2.Transaction;
 import org.skife.jdbi.v2.TransactionIsolationLevel;
 import org.skife.jdbi.v2.TransactionStatus;
 
-import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.clock.Clock;
+import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.Entity;
 
@@ -71,6 +71,10 @@ public class EntitySqlDaoTransactionalJdbiWrapper {
         return entitySqlDao.inTransaction(TransactionIsolationLevel.READ_COMMITTED, new JdbiTransaction<ReturnType, EntityModelDao<Entity>, Entity>(entitySqlDaoTransactionWrapper));
     }
 
+    public <M extends EntityModelDao<E>, E extends Entity, T extends EntitySqlDao<M, E>> T onDemand(final Class<T> sqlObjectType) {
+        return dbi.onDemand(sqlObjectType);
+    }
+
     /**
      * @param entitySqlDaoTransactionWrapper transaction to execute
      * @param <ReturnType>                   object type to return from the transaction
diff --git a/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java b/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
new file mode 100644
index 0000000..e766255
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.entity;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableList;
+
+// Assumes the original offset starts at zero.
+public class DefaultPagination<T> implements Pagination<T> {
+
+    private final Long currentOffset;
+    private final Long limit;
+    private final Long totalNbRecords;
+    private final Long maxNbRecords;
+    private final Iterator<T> delegateIterator;
+
+    // Builder when the streaming API can't be used
+    // Notes: elements should be the entire records set (regardless of filtering) otherwise maxNbRecords won't be accurate
+    public static <T> Pagination<T> build(final Long offset, final Long limit, final Collection<T> elements) {
+        final List<T> allResults = ImmutableList.<T>copyOf(elements);
+
+        final List<T> results;
+        if (offset >= allResults.size()) {
+            results = ImmutableList.<T>of();
+        } else if (offset + limit > allResults.size()) {
+            results = allResults.subList(offset.intValue(), allResults.size());
+        } else {
+            results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+        }
+        return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) allResults.size(), results.iterator());
+    }
+
+    // Constructor for DAO -> API bridge
+    public DefaultPagination(final Pagination original, final Long limit, final Iterator<T> delegate) {
+        this(original.getCurrentOffset(), limit, original.getTotalNbRecords(), original.getMaxNbRecords(), delegate);
+    }
+
+    // Constructor for DAO getAll calls
+    public DefaultPagination(final Long maxNbRecords, final Iterator<T> results) {
+        this(0L, Long.MAX_VALUE, maxNbRecords, maxNbRecords, results);
+    }
+
+    public DefaultPagination(final Long currentOffset, final Long limit,
+                             @Nullable final Long totalNbRecords, @Nullable final Long maxNbRecords,
+                             final Iterator<T> delegateIterator) {
+        this.currentOffset = currentOffset;
+        this.limit = limit;
+        this.totalNbRecords = totalNbRecords;
+        this.maxNbRecords = maxNbRecords;
+        this.delegateIterator = delegateIterator;
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        return delegateIterator;
+    }
+
+    @Override
+    public Long getCurrentOffset() {
+        return currentOffset;
+    }
+
+    @Override
+    public Long getNextOffset() {
+        final long candidate = currentOffset + limit;
+        if (totalNbRecords != null && candidate >= totalNbRecords) {
+            // No more results
+            return null;
+        } else {
+            // When we don't know the total number of records, the next offset
+            // returned here won't make sense once the last result is returned.
+            // It is the responsibility of the client to handle the pagination stop condition
+            // in that case (i.e. check if there is no more results).
+            return candidate;
+        }
+    }
+
+    @Override
+    public Long getMaxNbRecords() {
+        return maxNbRecords;
+    }
+
+    @Override
+    public Long getTotalNbRecords() {
+        return totalNbRecords;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultPagination{");
+        sb.append("currentOffset=").append(currentOffset);
+        sb.append(", nextOffset=").append(getNextOffset());
+        sb.append(", totalNbRecords=").append(totalNbRecords);
+        sb.append(", maxNbRecords=").append(maxNbRecords);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    // Expensive! Will compare the content of the iterator
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultPagination that = (DefaultPagination) o;
+
+        if (totalNbRecords != null ? !totalNbRecords.equals(that.totalNbRecords) : that.totalNbRecords != null) {
+            return false;
+        }
+        if (maxNbRecords != null ? !maxNbRecords.equals(that.maxNbRecords) : that.maxNbRecords != null) {
+            return false;
+        }
+        if (currentOffset != null ? !currentOffset.equals(that.currentOffset) : that.currentOffset != null) {
+            return false;
+        }
+        if (delegateIterator != null ? !ImmutableList.<T>copyOf(delegateIterator).equals(ImmutableList.<T>copyOf(that.delegateIterator)) : that.delegateIterator != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = currentOffset != null ? currentOffset.hashCode() : 0;
+        result = 31 * result + (totalNbRecords != null ? totalNbRecords.hashCode() : 0);
+        result = 31 * result + (maxNbRecords != null ? maxNbRecords.hashCode() : 0);
+        result = 31 * result + (delegateIterator != null ? delegateIterator.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDefinitionDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDefinitionDao.java
index 8bf21dc..019c222 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDefinitionDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDefinitionDao.java
@@ -17,6 +17,7 @@
 package com.ning.billing.util.tag.dao;
 
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
@@ -29,25 +30,26 @@ import org.slf4j.LoggerFactory;
 import com.ning.billing.BillingExceptionBase;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.bus.api.PersistentBus;
-import com.ning.billing.util.api.TagDefinitionApiException;
-import com.ning.billing.util.audit.ChangeType;
-import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.clock.Clock;
+import com.ning.billing.events.TagDefinitionInternalEvent;
+import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.audit.ChangeType;
+import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
-import com.ning.billing.events.TagDefinitionInternalEvent;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.TagDefinition;
 import com.ning.billing.util.tag.api.user.TagEventBuilder;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
 public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> implements TagDefinitionDao {
@@ -71,8 +73,10 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
             @Override
             public List<TagDefinitionModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
                 // Get user definitions from the database
+                final TagDefinitionSqlDao tagDefinitionSqlDao = entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class);
+                final Iterator<TagDefinitionModelDao> all = tagDefinitionSqlDao.getAll(context);
                 final List<TagDefinitionModelDao> definitionList = new LinkedList<TagDefinitionModelDao>();
-                definitionList.addAll(entitySqlDaoWrapperFactory.become(TagDefinitionSqlDao.class).get(context));
+                Iterators.addAll(definitionList, all);
 
                 // Add control tag definitions
                 for (final ControlTagType controlTag : ControlTagType.values()) {
@@ -240,7 +244,7 @@ public class DefaultTagDefinitionDao extends EntityDaoBase<TagDefinitionModelDao
                                      tagEventBuilder.newControlTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
                                                                                           context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken()) :
                                      tagEventBuilder.newUserTagDefinitionCreationEvent(tagDefinition.getId(), tagDefinition,
-                                             context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
+                                                                                       context.getAccountRecordId(), context.getTenantRecordId(), context.getUserToken());
 
                 break;
             case DELETE:
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionDao.java
index 76dcceb..6e9e6c4 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/TagDefinitionDao.java
@@ -20,16 +20,16 @@ import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
-import com.ning.billing.util.api.TagDefinitionApiException;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.entity.dao.EntityDao;
+import com.ning.billing.util.tag.TagDefinition;
 
-public interface TagDefinitionDao {
+public interface TagDefinitionDao extends EntityDao<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> {
 
     public List<TagDefinitionModelDao> getTagDefinitions(InternalTenantContext context);
 
-    public TagDefinitionModelDao getById(UUID definitionId, InternalTenantContext context);
-
     public TagDefinitionModelDao getByName(String definitionName, InternalTenantContext context);
 
     public List<TagDefinitionModelDao> getByIds(Collection<UUID> definitionIds, InternalTenantContext context);
diff --git a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
index 5ee87d6..1bc6a90 100644
--- a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -133,13 +133,36 @@ allHistoryTableValues() ::= <<
 CHECK_TENANT(prefix) ::= "<prefix>tenant_record_id = :tenantRecordId"
 AND_CHECK_TENANT(prefix) ::= "and <CHECK_TENANT(prefix)>"
 
-get(limit) ::= <<
+getFoundRows() ::= <<
+select FOUND_ROWS();
+>>
+
+getAll() ::= <<
 select
 <allTableFields("t.")>
 from <tableName()> t
 where <CHECK_TENANT("t.")>
 <andCheckSoftDeletionWithComma("t.")>
-<if(limit)>limit :limit<endif>
+;
+>>
+
+get(offset, rowCount, orderBy) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields("t.")>
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
+order by :orderBy
+limit :offset, :rowCount
+;
+>>
+
+getCount() ::= <<
+select
+count(1) as count
+from <tableName()> t
+where <CHECK_TENANT("t.")>
+<andCheckSoftDeletionWithComma("t.")>
 ;
 >>
 
diff --git a/util/src/test/java/com/ning/billing/mock/api/MockAccountUserApi.java b/util/src/test/java/com/ning/billing/mock/api/MockAccountUserApi.java
index 488b8d8..c29d635 100644
--- a/util/src/test/java/com/ning/billing/mock/api/MockAccountUserApi.java
+++ b/util/src/test/java/com/ning/billing/mock/api/MockAccountUserApi.java
@@ -16,7 +16,6 @@
 
 package com.ning.billing.mock.api;
 
-import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
@@ -33,6 +32,8 @@ import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.mock.MockAccountBuilder;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 public class MockAccountUserApi implements AccountUserApi {
 
@@ -107,7 +108,7 @@ public class MockAccountUserApi implements AccountUserApi {
     }
 
     @Override
-    public List<Account> searchAccounts(final String searchKey, final TenantContext tenantContext) {
+    public Pagination<Account> searchAccounts(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) {
         final List<Account> results = new LinkedList<Account>();
         for (final Account account : accounts) {
             if ((account.getName() != null && account.getName().contains(searchKey)) ||
@@ -117,12 +118,12 @@ public class MockAccountUserApi implements AccountUserApi {
                 results.add(account);
             }
         }
-        return results;
+        return DefaultPagination.<Account>build(offset, limit, results);
     }
 
     @Override
-    public List<Account> getAccounts(final TenantContext context) {
-        return new ArrayList<Account>(accounts);
+    public Pagination<Account> getAccounts(final Long offset, final Long limit, final TenantContext context) {
+        return DefaultPagination.<Account>build(offset, limit, accounts);
     }
 
     @Override
diff --git a/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java b/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
index 0f7f467..8521cb9 100644
--- a/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
+++ b/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
@@ -21,8 +21,8 @@ import java.util.List;
 import java.util.UUID;
 
 import com.ning.billing.ObjectType;
-import com.ning.billing.util.api.CustomFieldApiException;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.api.CustomFieldApiException;
 import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.entity.dao.MockEntityDaoBase;
 
@@ -31,7 +31,7 @@ public class MockCustomFieldDao extends MockEntityDaoBase<CustomFieldModelDao, C
     @Override
     public List<CustomFieldModelDao> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final InternalTenantContext context) {
         final List<CustomFieldModelDao> result = new ArrayList<CustomFieldModelDao>();
-        final List<CustomFieldModelDao> all = get(context);
+        final Iterable<CustomFieldModelDao> all = getAll(context);
         for (final CustomFieldModelDao cur : all) {
             if (cur.getObjectId().equals(objectId) && cur.getObjectType() == objectType) {
                 result.add(cur);
diff --git a/util/src/test/java/com/ning/billing/util/dao/TestPagination.java b/util/src/test/java/com/ning/billing/util/dao/TestPagination.java
new file mode 100644
index 0000000..0f0832a
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/dao/TestPagination.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.dao;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuiteWithEmbeddedDB;
+import com.ning.billing.util.tag.dao.TagDefinitionModelDao;
+import com.ning.billing.util.tag.dao.TagDefinitionSqlDao;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestPagination extends UtilTestSuiteWithEmbeddedDB {
+
+    @Test(groups = "slow", description = "Test Pagination: basic SqlDAO and DAO calls")
+    public void testTagDefinitionsPagination() throws Exception {
+        final TagDefinitionSqlDao tagDefinitionSqlDao = dbi.onDemand(TagDefinitionSqlDao.class);
+
+        for (int i = 0; i < 10; i++) {
+            final String definitionName = "name-" + i;
+            final String description = "description-" + i;
+            tagDefinitionDao.create(definitionName, description, internalCallContext);
+        }
+
+        // Tests via SQL dao directly
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.getAll(internalCallContext)).size(), 10);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, 100L, "recordId", internalCallContext)).size(), 10);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(5L, 100L, "recordId", internalCallContext)).size(), 5);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(5L, 10L, "recordId", internalCallContext)).size(), 5);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, 5L, "recordId", internalCallContext)).size(), 5);
+        for (int i = 0; i < 10; i++) {
+            final List<TagDefinitionModelDao> tagDefinitions = ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionSqlDao.get(0L, (long) i, "recordId", internalCallContext));
+            Assert.assertEquals(tagDefinitions.size(), i);
+
+            for (int j = 0; j < tagDefinitions.size(); j++) {
+                Assert.assertEquals(tagDefinitions.get(j).getName(), "name-" + j);
+                Assert.assertEquals(tagDefinitions.get(j).getDescription(), "description-" + j);
+            }
+        }
+
+        // Tests via DAO (to test EntityDaoBase)
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.getAll(internalCallContext)).size(), 10);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, 100L, internalCallContext)).size(), 10);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(5L, 100L, internalCallContext)).size(), 5);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(5L, 10L, internalCallContext)).size(), 5);
+        Assert.assertEquals(ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, 5L, internalCallContext)).size(), 5);
+        for (int i = 0; i < 10; i++) {
+            final List<TagDefinitionModelDao> tagDefinitions = ImmutableList.<TagDefinitionModelDao>copyOf(tagDefinitionDao.get(0L, (long) i, internalCallContext));
+            Assert.assertEquals(tagDefinitions.size(), i);
+
+            for (int j = 0; j < tagDefinitions.size(); j++) {
+                Assert.assertEquals(tagDefinitions.get(j).getName(), "name-" + j);
+                Assert.assertEquals(tagDefinitions.get(j).getDescription(), "description-" + j);
+            }
+        }
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
index 4daf73a..1306cfe 100644
--- a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
+++ b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
@@ -102,29 +102,30 @@ public class TestStringTemplateInheritance extends UtilTestSuiteNoDB {
                                                                                      "where t.target_record_id = :targetRecordId\n" +
                                                                                      "and t.tenant_record_id = :tenantRecordId\n" +
                                                                                      ";");
-        Assert.assertEquals(kombucha.getInstanceOf("get").toString(), "select\n" +
-                                                                      "  t.record_id\n" +
-                                                                      ", t.id\n" +
-                                                                      ", t.tea\n" +
-                                                                      ", t.mushroom\n" +
-                                                                      ", t.sugar\n" +
-                                                                      ", t.account_record_id\n" +
-                                                                      ", t.tenant_record_id\n" +
-                                                                      "from kombucha t\n" +
-                                                                      "where t.tenant_record_id = :tenantRecordId\n" +
-                                                                      ";");
-        Assert.assertEquals(kombucha.getInstanceOf("get", ImmutableMap.<String, String>of("limit", "12")).toString(), "select\n" +
-                                                                                                                      "  t.record_id\n" +
-                                                                                                                      ", t.id\n" +
-                                                                                                                      ", t.tea\n" +
-                                                                                                                      ", t.mushroom\n" +
-                                                                                                                      ", t.sugar\n" +
-                                                                                                                      ", t.account_record_id\n" +
-                                                                                                                      ", t.tenant_record_id\n" +
-                                                                                                                      "from kombucha t\n" +
-                                                                                                                      "where t.tenant_record_id = :tenantRecordId\n" +
-                                                                                                                      "limit :limit\n" +
-                                                                                                                      ";");
+        Assert.assertEquals(kombucha.getInstanceOf("getAll").toString(), "select\n" +
+                                                                         "  t.record_id\n" +
+                                                                         ", t.id\n" +
+                                                                         ", t.tea\n" +
+                                                                         ", t.mushroom\n" +
+                                                                         ", t.sugar\n" +
+                                                                         ", t.account_record_id\n" +
+                                                                         ", t.tenant_record_id\n" +
+                                                                         "from kombucha t\n" +
+                                                                         "where t.tenant_record_id = :tenantRecordId\n" +
+                                                                         ";");
+        Assert.assertEquals(kombucha.getInstanceOf("get", ImmutableMap.<String, String>of("orderBy", "recordId", "offset", "3", "rowCount", "12")).toString(), "select SQL_CALC_FOUND_ROWS\n" +
+                                                                                                                                                               "  t.record_id\n" +
+                                                                                                                                                               ", t.id\n" +
+                                                                                                                                                               ", t.tea\n" +
+                                                                                                                                                               ", t.mushroom\n" +
+                                                                                                                                                               ", t.sugar\n" +
+                                                                                                                                                               ", t.account_record_id\n" +
+                                                                                                                                                               ", t.tenant_record_id\n" +
+                                                                                                                                                               "from kombucha t\n" +
+                                                                                                                                                               "where t.tenant_record_id = :tenantRecordId\n" +
+                                                                                                                                                               "order by :orderBy\n" +
+                                                                                                                                                               "limit :offset, :rowCount\n" +
+                                                                                                                                                               ";");
         Assert.assertEquals(kombucha.getInstanceOf("test").toString(), "select\n" +
                                                                        "  t.record_id\n" +
                                                                        ", t.id\n" +
diff --git a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java
index 24abcd5..0b2cad2 100644
--- a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java
+++ b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritanceWithJdbi.java
@@ -20,7 +20,6 @@ import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import com.ning.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
 import com.ning.billing.util.UtilTestSuiteWithEmbeddedDB;
 import com.ning.billing.util.entity.Entity;
 import com.ning.billing.util.entity.dao.EntityModelDao;
@@ -48,6 +47,6 @@ public class TestStringTemplateInheritanceWithJdbi extends UtilTestSuiteWithEmbe
         Assert.assertEquals(dao.isIsTimeForKombucha(), clock.getUTCNow().getHourOfDay() == 17);
 
         // Verify inherited templates
-        Assert.assertEquals(dao.get(internalCallContext).size(), 0);
+        Assert.assertFalse(dao.getAll(internalCallContext).hasNext());
     }
 }
diff --git a/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java b/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
index b7ba4a2..0aabc34 100644
--- a/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
+++ b/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
@@ -26,8 +26,11 @@ import java.util.concurrent.atomic.AtomicLong;
 import com.ning.billing.BillingExceptionBase;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
 public class MockEntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> implements EntityDao<M, E, U> {
@@ -62,12 +65,22 @@ public class MockEntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U 
     }
 
     @Override
-    public List<M> get(final InternalTenantContext context) {
+    public Pagination<M> getAll(final InternalTenantContext context) {
         final List<M> result = new ArrayList<M>();
         for (final Map<Long, M> cur : entities.values()) {
             result.add(cur.values().iterator().next());
         }
-        return result;
+        return new DefaultPagination<M>(getCount(context), result.iterator());
+    }
+
+    @Override
+    public Pagination<M> get(final Long offset, final Long limit, final InternalTenantContext context) {
+        return DefaultPagination.<M>build(offset, limit, ImmutableList.<M>copyOf(getAll(context)));
+    }
+
+    @Override
+    public Long getCount(final InternalTenantContext context) {
+        return (long) entities.keySet().size();
     }
 
     public void update(final M entity, final InternalCallContext context) {
diff --git a/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java b/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java
new file mode 100644
index 0000000..2600ff1
--- /dev/null
+++ b/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.entity;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.util.UtilTestSuiteNoDB;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultPagination extends UtilTestSuiteNoDB {
+
+    @Test(groups = "fast", description = "Test Util: Pagination builder tests")
+    public void testDefaultPaginationBuilder() throws Exception {
+        Assert.assertEquals(DefaultPagination.<Integer>build(0L, 0L, ImmutableList.<Integer>of()), expectedOf(0L, 0L, 0L, ImmutableList.<Integer>of()));
+        Assert.assertEquals(DefaultPagination.<Integer>build(0L, 10L, ImmutableList.<Integer>of()), expectedOf(0L, 0L, 0L, ImmutableList.<Integer>of()));
+        Assert.assertEquals(DefaultPagination.<Integer>build(10L, 0L, ImmutableList.<Integer>of()), expectedOf(10L, 0L, 0L, ImmutableList.<Integer>of()));
+        Assert.assertEquals(DefaultPagination.<Integer>build(10L, 10L, ImmutableList.<Integer>of()), expectedOf(10L, 0L, 0L, ImmutableList.<Integer>of()));
+
+        Assert.assertEquals(DefaultPagination.<Integer>build(0L, 0L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 0L, 5L, ImmutableList.<Integer>of()));
+        Assert.assertEquals(DefaultPagination.<Integer>build(0L, 10L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 5L, 5L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(4L, 10L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(4L, 1L, 5L, ImmutableList.<Integer>of(5)));
+
+        Assert.assertEquals(DefaultPagination.<Integer>build(0L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(0L, 3L, 5L, ImmutableList.<Integer>of(1, 2, 3)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(1L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(1L, 3L, 5L, ImmutableList.<Integer>of(2, 3, 4)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(2L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(2L, 3L, 5L, ImmutableList.<Integer>of(3, 4, 5)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(3L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(3L, 2L, 5L, ImmutableList.<Integer>of(4, 5)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(4L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(4L, 1L, 5L, ImmutableList.<Integer>of(5)));
+        Assert.assertEquals(DefaultPagination.<Integer>build(5L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(5L, 0L, 5L, ImmutableList.<Integer>of()));
+    }
+
+    private Pagination<Integer> expectedOf(final Long currentOffset, final Long totalNbRecords,
+                                           final Long maxNbRecords, final List<Integer> delegate) {
+        return new DefaultPagination<Integer>(currentOffset, Long.MAX_VALUE, totalNbRecords, maxNbRecords, delegate.iterator());
+    }
+}
diff --git a/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDefinitionDao.java b/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDefinitionDao.java
index 9232b1d..592d964 100644
--- a/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDefinitionDao.java
+++ b/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDefinitionDao.java
@@ -23,11 +23,13 @@ import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
-import com.ning.billing.util.api.TagDefinitionApiException;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.entity.dao.MockEntityDaoBase;
+import com.ning.billing.util.tag.TagDefinition;
 
-public class MockTagDefinitionDao implements TagDefinitionDao {
+public class MockTagDefinitionDao extends MockEntityDaoBase<TagDefinitionModelDao, TagDefinition, TagDefinitionApiException> implements TagDefinitionDao {
 
     private final Map<String, TagDefinitionModelDao> tags = new ConcurrentHashMap<String, TagDefinitionModelDao>();
 
@@ -56,11 +58,6 @@ public class MockTagDefinitionDao implements TagDefinitionDao {
     }
 
     @Override
-    public TagDefinitionModelDao getById(final UUID definitionId, final InternalTenantContext context) {
-        return null;
-    }
-
-    @Override
     public List<TagDefinitionModelDao> getByIds(final Collection<UUID> definitionIds, final InternalTenantContext context) {
         return null;
     }