killbill-memoizeit

account: invoice: payment: initial pagination implementation Signed-off-by:

10/21/2013 11:21:53 AM

Changes

pom.xml 2(+1 -1)

Details

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..cd2c53e 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,29 @@ 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,
+                                              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,
+                                              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..b6ed783 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 rowCount, 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..7c71ea2 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,18 @@ 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>>() {
-            @Override
-            public List<AccountModelDao> inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
-                return entitySqlDaoWrapperFactory.become(AccountSqlDao.class).searchAccounts(searchKey, context);
-            }
-        });
+    public Pagination<AccountModelDao> searchAccounts(final String searchKey, final Long offset, final Long rowCount, 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 AccountSqlDao accountSqlDao = transactionalSqlDao.onDemand(AccountSqlDao.class);
+
+        // Note: the connection will be busy as we stream the results out: hence we cannot use
+        // SQL_CALC_FOUND_ROWS / FOUND_ROWS and we don't know the total number of results (in this case,
+        // performing a second time the search just to get the count is too expensive to be worth it).
+        final Long count = null;
+
+        final Iterator<AccountModelDao> results = accountSqlDao.searchAccounts(searchKey, offset, rowCount, context);
+        return new DefaultPagination<AccountModelDao>(offset, rowCount, count, results);
     }
 
     @Override
@@ -236,13 +244,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..561566e 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) ::= <<
+searchAccounts(searchKey, offset, rowCount) ::= <<
 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()>
+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/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..0a53eb3 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,20 @@ 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,
+                                              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 daed996..c5540cb 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,6 +16,8 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -36,8 +38,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 +96,13 @@ 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.Multimap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -154,18 +159,37 @@ public class AccountResource extends JaxRsResourceBase {
     @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 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_RESULTS, accounts.getTotalNbResults())
+                       .header(HDR_PAGINATION_NB_RESULTS, accounts.getNbResults())
+                       .header(HDR_PAGINATION_NB_RESULTS_FROM_OFFSET, accounts.getNbResultsFromOffset())
+                       .build();
     }
 
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index 8f91ef2..07e7ac3 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_RESULTS = "X-Killbill-Pagination-TotalNbResults";
+    public static String HDR_PAGINATION_NB_RESULTS = "X-Killbill-Pagination-NbResults";
+    public static String HDR_PAGINATION_NB_RESULTS_FROM_OFFSET = "X-Killbill-Pagination-NbResultsFromOffset";
 
     /*
      * 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";
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..2af1d58 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,9 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -30,8 +31,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 +52,10 @@ 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.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -96,34 +99,55 @@ public class PaymentMethodResource extends JaxRsResourceBase {
     @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 List<PaymentMethodJson> json = Lists.transform(paymentMethods, new Function<PaymentMethod, PaymentMethodJson>() {
+        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_RESULTS, paymentMethods.getTotalNbResults())
+                       .header(HDR_PAGINATION_NB_RESULTS, paymentMethods.getNbResults())
+                       .header(HDR_PAGINATION_NB_RESULTS_FROM_OFFSET, paymentMethods.getNbResultsFromOffset())
+                       .build();
     }
 
     @DELETE
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..dffd273 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,38 @@ 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 getTotalNbResults() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNbResults() {
+                return null;
+            }
+
+            @Override
+            public Long getNbResultsFromOffset() {
+                return null;
+            }
+
+            @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..0d4f742 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,38 @@ 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 getTotalNbResults() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNbResults() {
+                return null;
+            }
+
+            @Override
+            public Long getNbResultsFromOffset() {
+                return null;
+            }
+
+            @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..aeb556b 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,14 @@ import java.util.UUID;
 
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
+import com.ning.billing.callcontext.InternalCallContext;
 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.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
@@ -173,13 +174,13 @@ 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> 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..9f05903 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,16 @@ 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.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 +157,59 @@ 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> 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>();
 
-        // 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 = 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 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> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-        final List<PaymentMethodPlugin> paymentMethods;
+        final Pagination<PaymentMethodPlugin> paymentMethods;
         try {
-            paymentMethods = pluginApi.searchPaymentMethods(searchKey, buildTenantContext(internalTenantContext));
+            paymentMethods = pluginApi.searchPaymentMethods(searchKey, offset, limit, buildTenantContext(internalTenantContext));
         } catch (PaymentPluginApiException e) {
             throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
         }
 
-        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;
-            }
-
-            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;
-            }
-
-            results.add(new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin));
-        }
-
-        return results;
+        return new DefaultPagination<PaymentMethod>(paymentMethods,
+                                                    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..396a17d 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 searchKy, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new DefaultPagination<PaymentMethodPlugin>(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..68be740 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.0-SNAPSHOT</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..477a305 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 rowCount, 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..f2e2462 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,38 @@ 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 rowCount, final InternalTenantContext context) {
+        // See notes above
+        final EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(realSqlDao);
+        final Long count = sqlDao.getCount(context);
+
+        final Iterator<M> results = sqlDao.get(offset, rowCount, getNaturalOrderingColumns(), context);
+        return new DefaultPagination<M>(offset, rowCount, count, results);
+    }
+
+    @Override
+    public Long getCount(final InternalTenantContext context) {
+        return 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> 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..d6f8fbe 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,23 @@ public interface EntitySqlDao<M extends EntityModelDao<E>, E extends Entity> ext
                             @BindBean final InternalTenantContext context);
 
     @SqlQuery
-    public List<M> get(@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<M> getAll(@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> 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..0000879
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
@@ -0,0 +1,143 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+
+public class DefaultPagination<T> implements Pagination<T> {
+
+    private final Long currentOffset;
+    private final Long approximateNbResults;
+    private final Long approximateTotalNbResults;
+    private final Iterator<T> delegateIterator;
+
+    // Builder when the streaming API can't be used
+    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, (long) results.size(), (long) allResults.size(), results.iterator());
+    }
+
+    // Constructor for DAO -> API bridge
+    public DefaultPagination(final Pagination original, final Iterator<T> delegate) {
+        this(original.getCurrentOffset(), original.getNbResults(), original.getTotalNbResults(), delegate);
+    }
+
+    // Constructor for DAO getAll calls
+    public DefaultPagination(final Long approximateTotalNbResults, final Iterator<T> results) {
+        this(0L, approximateTotalNbResults, approximateTotalNbResults, results);
+    }
+
+    public DefaultPagination(final Long currentOffset, final Long approximateNbResults,
+                             final Long approximateTotalNbResults, final Iterator<T> delegateIterator) {
+        this.currentOffset = currentOffset;
+        this.approximateNbResults = approximateNbResults;
+        this.approximateTotalNbResults = approximateTotalNbResults;
+        this.delegateIterator = delegateIterator;
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        return delegateIterator;
+    }
+
+    @Override
+    public Long getCurrentOffset() {
+        return currentOffset;
+    }
+
+    @Override
+    public Long getNextOffset() {
+        return currentOffset + approximateNbResults;
+    }
+
+    @Override
+    public Long getTotalNbResults() {
+        return approximateTotalNbResults;
+    }
+
+    @Override
+    public Long getNbResults() {
+        return approximateNbResults;
+    }
+
+    @Override
+    public Long getNbResultsFromOffset() {
+        return approximateNbResults - getNextOffset() + 1;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("DefaultPagination{");
+        sb.append("currentOffset=").append(currentOffset);
+        sb.append(", nextOffset=").append(getNextOffset());
+        sb.append(", approximateNbResults=").append(approximateNbResults);
+        sb.append(", approximateTotalNbResults=").append(approximateTotalNbResults);
+        sb.append(", approximateNbResultsFromOffset=").append(getNbResultsFromOffset());
+        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 (approximateNbResults != null ? !approximateNbResults.equals(that.approximateNbResults) : that.approximateNbResults != null) {
+            return false;
+        }
+        if (approximateTotalNbResults != null ? !approximateTotalNbResults.equals(that.approximateTotalNbResults) : that.approximateTotalNbResults != 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 + (approximateNbResults != null ? approximateNbResults.hashCode() : 0);
+        result = 31 * result + (approximateTotalNbResults != null ? approximateTotalNbResults.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..646d457 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,32 @@ allHistoryTableValues() ::= <<
 CHECK_TENANT(prefix) ::= "<prefix>tenant_record_id = :tenantRecordId"
 AND_CHECK_TENANT(prefix) ::= "and <CHECK_TENANT(prefix)>"
 
-get(limit) ::= <<
+getAll() ::= <<
 select
 <allTableFields("t.")>
 from <tableName()> t
 where <CHECK_TENANT("t.")>
 <andCheckSoftDeletionWithComma("t.")>
-<if(limit)>limit :limit<endif>
+;
+>>
+
+get(offset, rowCount, orderBy) ::= <<
+select
+<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..bc1c714 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\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..d51217e 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 rowCount, final InternalTenantContext context) {
+        return DefaultPagination.<M>build(offset, rowCount, 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..cfb29f5
--- /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 approximateNbResults,
+                                           final Long approximateTotalNbResults, final List<Integer> delegate) {
+        return new DefaultPagination<Integer>(currentOffset, approximateNbResults, approximateTotalNbResults, 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;
     }