Merge changes from topic "add-members-subgroups-to-group-index"

* changes:
  Add fields for members and subgroups to the group index
  Use the term 'subgroup' instead of 'includes' where possible
  Avoid unnecessary loading of members and subgroups
  Cache instances of type InternalGroup instead of AccountGroup by UUID
  Base group index on new class InternalGroup
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 657f498..7eac992 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -116,7 +116,7 @@
 
 [[includes]]
 --
-* `INCLUDES`: include list of directly included groups.
+* `INCLUDES`: include list of direct subgroups.
 --
 
 [[members]]
@@ -1129,13 +1129,13 @@
   ]
 ----
 
-[[delete-group-member]]
-=== Delete Group Member
+[[remove-group-member]]
+=== Remove Group Member
 --
 'DELETE /groups/link:#group-id[\{group-id\}]/members/link:rest-api-accounts.html#account-id[\{account-id\}]'
 --
 
-Deletes a user from a Gerrit internal group.
+Removes a user from a Gerrit internal group.
 
 .Request
 ----
@@ -1147,15 +1147,15 @@
   HTTP/1.1 204 No Content
 ----
 
-[[delete-group-members]]
-=== Delete Group Members
+[[remove-group-members]]
+=== Remove Group Members
 --
 'POST /groups/link:#group-id[\{group-id\}]/members.delete'
 --
 
-Delete one or several users from a Gerrit internal group.
+Removes one or several users from a Gerrit internal group.
 
-The users to be deleted from the group must be provided in the request
+The users to be removed from the group must be provided in the request
 body as a link:#members-input[MembersInput] entity.
 
 .Request
@@ -1176,16 +1176,16 @@
   HTTP/1.1 204 No Content
 ----
 
-[[group-include-endpoints]]
-== Group Include Endpoints
+[[subgroup-endpoints]]
+== Subgroup Endpoints
 
-[[included-groups]]
-=== List Included Groups
+[[list-subgroups]]
+=== List Subgroups
 --
 'GET /groups/link:#group-id[\{group-id\}]/groups/'
 --
 
-Lists the directly included groups of a group.
+Lists the direct subgroups of a group.
 
 As result a list of link:#group-info[GroupInfo] entries is returned.
 The entries in the list are sorted by group name and UUID.
@@ -1217,13 +1217,13 @@
   ]
 ----
 
-[[get-included-group]]
-=== Get Included Group
+[[get-subgroup]]
+=== Get Subgroup
 --
 'GET /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
 --
 
-Retrieves an included group.
+Retrieves a subgroup.
 
 .Request
 ----
@@ -1231,7 +1231,7 @@
 ----
 
 As response a link:#group-info[GroupInfo] entity is returned that
-describes the included group.
+describes the subgroup.
 
 .Response
 ----
@@ -1253,13 +1253,13 @@
   }
 ----
 
-[[include-group]]
-=== Include Group
+[[add-subgroup]]
+=== Add Subgroup
 --
 'PUT /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
 --
 
-Includes an internal or external group into a Gerrit internal group.
+Adds an internal or external group as subgroup to a Gerrit internal group.
 External groups must be specified using the UUID.
 
 .Request
@@ -1268,7 +1268,7 @@
 ----
 
 As response a link:#group-info[GroupInfo] entity is returned that
-describes the included group.
+describes the subgroup.
 
 .Response
 ----
@@ -1290,11 +1290,11 @@
   }
 ----
 
-The request also succeeds if the group is already included in this
-group, but then the HTTP response code is `200 OK`.
+The request also succeeds if the group is already a subgroup of this
+group.
 
-[[include-groups]]
-=== Include Groups
+[[add-subgroups]]
+=== Add Subgroups
 --
 'POST /groups/link:#group-id[\{group-id\}]/groups'
 --
@@ -1305,10 +1305,10 @@
 'POST /groups/link:#group-id[\{group-id\}]/groups.add'
 --
 
-Includes one or several groups into a Gerrit internal group.
+Adds one or several groups as subgroups to a Gerrit internal group.
 
-The groups to be included into the group must be provided in the
-request body as a link:#groups-input[GroupsInput] entity.
+The subgroups to be added must be provided in the request body as a
+link:#groups-input[GroupsInput] entity.
 
 .Request
 ----
@@ -1327,8 +1327,8 @@
 returned that describes the groups that were specified in the
 link:#groups-input[GroupsInput]. A link:#group-info[GroupInfo] entity
 is returned for each group specified in the input, independently of
-whether the group was newly included into the group or whether the
-group was already included in the group.
+whether the group was newly added as subgroup or whether the
+group was already a subgroup of the group.
 
 .Response
 ----
@@ -1363,13 +1363,13 @@
   ]
 ----
 
-[[delete-included-group]]
-=== Delete Included Group
+[[remove-subgroup]]
+=== Remove Subgroup
 --
 'DELETE /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
 --
 
-Deletes an included group from a Gerrit internal group.
+Removes a subgroup from a Gerrit internal group.
 
 .Request
 ----
@@ -1381,16 +1381,16 @@
   HTTP/1.1 204 No Content
 ----
 
-[[delete-included-groups]]
-=== Delete Included Groups
+[[remove-subgroups]]
+=== Remove Subgroups
 --
 'POST /groups/link:#group-id[\{group-id\}]/groups.delete'
 --
 
-Delete one or several included groups from a Gerrit internal group.
+Removes one or several subgroups from a Gerrit internal group.
 
-The groups to be deleted from the group must be provided in the request
-body as a link:#groups-input[GroupsInput] entity.
+The subgroups to be removed must be provided in the request body as a
+link:#groups-input[GroupsInput] entity.
 
 .Request
 ----
@@ -1495,9 +1495,9 @@
 entities describing the direct members. +
 Only set if link:#members[members] are requested.
 |`includes`    |optional, only for internal groups|
-A list of link:#group-info[GroupInfo] entities describing the directly
-included groups. +
-Only set if link:#includes[included groups] are requested.
+A list of link:#group-info[GroupInfo] entities describing the direct
+subgroups. +
+Only set if link:#includes[subgroups] are requested.
 |===========================
 
 The type of a group can be deduced from the group's UUID:
diff --git a/Documentation/user-search-groups.txt b/Documentation/user-search-groups.txt
index fccad65..6fa8dbb 100644
--- a/Documentation/user-search-groups.txt
+++ b/Documentation/user-search-groups.txt
@@ -59,6 +59,17 @@
 +
 Matches groups that have the UUID 'UUID'.
 
+[[member]]
+member:'MEMBER'::
++
+Matches groups that have the account represented by 'MEMBER' as a member.
+
+[[subgroup]]
+subgroup:'SUBGROUP'::
++
+Matches groups that have a subgroup whose name best matches 'SUBGROUP' or
+whose UUID is 'SUBGROUP'.
+
 == Magical Operators
 
 [[is-visible]]
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 13dcb77..dca2fb0 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -92,6 +92,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -352,7 +353,7 @@
     // removes all indexed data.
     // As a workaround, we simply reindex all available groups here.
     for (AccountGroup group : groupCache.all()) {
-      groupCache.evict(group);
+      groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
     }
 
     admin = accountCreator.admin();
@@ -1178,8 +1179,9 @@
       String g = createGroup("cla-test-group");
       GroupApi groupApi = gApi.groups().id(g);
       groupApi.description("CLA test group");
-      AccountGroup caGroup = groupCache.get(new AccountGroup.UUID(groupApi.detail().id));
-      GroupReference groupRef = GroupReference.forGroup(caGroup);
+      InternalGroup caGroup =
+          groupCache.get(new AccountGroup.UUID(groupApi.detail().id)).orElse(null);
+      GroupReference groupRef = new GroupReference(caGroup.getGroupUUID(), caGroup.getName());
       PermissionRule rule = new PermissionRule(groupRef);
       rule.setAction(PermissionRule.Action.ALLOW);
       ca = new ContributorAgreement("cla-test");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index e5816dd..cb0d768ef 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.group.CreateGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -44,9 +45,9 @@
 public class SuggestReviewersIT extends AbstractDaemonTest {
   @Inject private CreateGroup.Factory createGroupFactory;
 
-  private AccountGroup group1;
-  private AccountGroup group2;
-  private AccountGroup group3;
+  private InternalGroup group1;
+  private InternalGroup group2;
+  private InternalGroup group3;
 
   private TestAccount user1;
   private TestAccount user2;
@@ -234,8 +235,8 @@
   @GerritConfig(name = "addreviewer.maxAllowed", value = "2")
   @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value = "1")
   public void suggestReviewersGroupSizeConsiderations() throws Exception {
-    AccountGroup largeGroup = group("large");
-    AccountGroup mediumGroup = group("medium");
+    InternalGroup largeGroup = group("large");
+    InternalGroup mediumGroup = group("medium");
 
     // Both groups have Administrator as a member. Add two users to large
     // group to push it past maxAllowed, and one to medium group to push it
@@ -424,19 +425,19 @@
     return gApi.changes().id(changeId).suggestReviewers(query).withLimit(n).get();
   }
 
-  private AccountGroup group(String name) throws Exception {
+  private InternalGroup group(String name) throws Exception {
     GroupInfo group = createGroupFactory.create(name(name)).apply(TopLevelResource.INSTANCE, null);
-    return groupCache.get(new AccountGroup.UUID(group.id));
+    return groupCache.get(new AccountGroup.UUID(group.id)).orElse(null);
   }
 
-  private TestAccount user(String name, String fullName, String emailName, AccountGroup... groups)
+  private TestAccount user(String name, String fullName, String emailName, InternalGroup... groups)
       throws Exception {
-    String[] groupNames = Arrays.stream(groups).map(AccountGroup::getName).toArray(String[]::new);
+    String[] groupNames = Arrays.stream(groups).map(InternalGroup::getName).toArray(String[]::new);
     return accountCreator.create(
         name(name), name(emailName) + "@example.com", fullName, groupNames);
   }
 
-  private TestAccount user(String name, String fullName, AccountGroup... groups) throws Exception {
+  private TestAccount user(String name, String fullName, InternalGroup... groups) throws Exception {
     return user(name, fullName, name, groups);
   }
 
@@ -461,7 +462,7 @@
   private void assertReviewers(
       List<SuggestedReviewerInfo> actual,
       List<TestAccount> expectedUsers,
-      List<AccountGroup> expectedGroups) {
+      List<InternalGroup> expectedGroups) {
     List<Integer> actualAccountIds =
         actual
             .stream()
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
index ef3cfd0..c915cb9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -48,6 +48,7 @@
 
     AccountGroup.Id getId();
 
+    @Nullable
     String getDescription();
 
     AccountGroup.UUID getOwnerGroupUUID();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
index b7a06c5..25493e8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -51,6 +51,7 @@
       }
 
       @Override
+      @Nullable
       public String getDescription() {
         return group.getDescription();
       }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
deleted file mode 100644
index 1ac06db..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed 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.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.util.Set;
-
-public class GroupDetail {
-  private Set<Account.Id> members;
-  private Set<AccountGroup.UUID> includes;
-
-  public GroupDetail(Set<Account.Id> members, Set<AccountGroup.UUID> includes) {
-    this.members = members;
-    this.includes = includes;
-  }
-
-  public Set<Account.Id> getMembers() {
-    return members;
-  }
-
-  public Set<AccountGroup.UUID> getIncludes() {
-    return includes;
-  }
-}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index 38c4e23..6ca4ad5 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.group.GroupField;
 import com.google.gerrit.server.index.group.GroupIndex;
@@ -48,6 +49,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -55,12 +57,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class ElasticGroupIndex extends AbstractElasticIndex<AccountGroup.UUID, AccountGroup>
+public class ElasticGroupIndex extends AbstractElasticIndex<AccountGroup.UUID, InternalGroup>
     implements GroupIndex {
   static class GroupMapping {
     MappingProperties groups;
 
-    GroupMapping(Schema<AccountGroup> schema) {
+    GroupMapping(Schema<InternalGroup> schema) {
       this.groups = ElasticMapping.createMapping(schema);
     }
   }
@@ -79,14 +81,14 @@
       SitePaths sitePaths,
       Provider<GroupCache> groupCache,
       JestClientBuilder clientBuilder,
-      @Assisted Schema<AccountGroup> schema) {
+      @Assisted Schema<InternalGroup> schema) {
     super(cfg, sitePaths, schema, clientBuilder, GROUPS_PREFIX);
     this.groupCache = groupCache;
     this.mapping = new GroupMapping(schema);
   }
 
   @Override
-  public void replace(AccountGroup group) throws IOException {
+  public void replace(InternalGroup group) throws IOException {
     Bulk bulk =
         new Bulk.Builder()
             .defaultIndex(indexName)
@@ -104,7 +106,7 @@
   }
 
   @Override
-  public DataSource<AccountGroup> getSource(Predicate<AccountGroup> p, QueryOptions opts)
+  public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
       throws QueryParseException {
     return new QuerySource(p, opts);
   }
@@ -121,15 +123,15 @@
   }
 
   @Override
-  protected String getId(AccountGroup group) {
+  protected String getId(InternalGroup group) {
     return group.getGroupUUID().get();
   }
 
-  private class QuerySource implements DataSource<AccountGroup> {
+  private class QuerySource implements DataSource<InternalGroup> {
     private final Search search;
     private final Set<String> fields;
 
-    QuerySource(Predicate<AccountGroup> p, QueryOptions opts) throws QueryParseException {
+    QuerySource(Predicate<InternalGroup> p, QueryOptions opts) throws QueryParseException {
       QueryBuilder qb = queryBuilder.toQueryBuilder(p);
       fields = IndexUtils.groupFields(opts);
       SearchSourceBuilder searchSource =
@@ -156,9 +158,9 @@
     }
 
     @Override
-    public ResultSet<AccountGroup> read() throws OrmException {
+    public ResultSet<InternalGroup> read() throws OrmException {
       try {
-        List<AccountGroup> results = Collections.emptyList();
+        List<InternalGroup> results = Collections.emptyList();
         JestResult result = client.execute(search);
         if (result.isSucceeded()) {
           JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
@@ -166,21 +168,22 @@
             JsonArray json = obj.getAsJsonArray("hits");
             results = Lists.newArrayListWithCapacity(json.size());
             for (int i = 0; i < json.size(); i++) {
-              results.add(toAccountGroup(json.get(i)));
+              Optional<InternalGroup> internalGroup = toInternalGroup(json.get(i));
+              internalGroup.ifPresent(results::add);
             }
           }
         } else {
           log.error(result.getErrorMessage());
         }
-        final List<AccountGroup> r = Collections.unmodifiableList(results);
-        return new ResultSet<AccountGroup>() {
+        final List<InternalGroup> r = Collections.unmodifiableList(results);
+        return new ResultSet<InternalGroup>() {
           @Override
-          public Iterator<AccountGroup> iterator() {
+          public Iterator<InternalGroup> iterator() {
             return r.iterator();
           }
 
           @Override
-          public List<AccountGroup> toList() {
+          public List<InternalGroup> toList() {
             return r;
           }
 
@@ -199,7 +202,7 @@
       return search.toString();
     }
 
-    private AccountGroup toAccountGroup(JsonElement json) {
+    private Optional<InternalGroup> toInternalGroup(JsonElement json) {
       JsonElement source = json.getAsJsonObject().get("_source");
       if (source == null) {
         source = json.getAsJsonObject().get("fields");
diff --git a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 694348f..fac10eb 100644
--- a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -27,8 +27,8 @@
 import com.google.gerrit.elasticsearch.ElasticChangeIndex.ChangeMapping;
 import com.google.gerrit.elasticsearch.ElasticGroupIndex.GroupMapping;
 import com.google.gerrit.index.Schema;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
@@ -146,7 +146,7 @@
         .execute()
         .actionGet();
 
-    Schema<AccountGroup> groupSchema = GroupSchemaDefinitions.INSTANCE.getLatest();
+    Schema<InternalGroup> groupSchema = GroupSchemaDefinitions.INSTANCE.getLatest();
     GroupMapping groupMapping = new GroupMapping(groupSchema);
     nodeInfo
         .node
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index 0d4742b..fe85eaa 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -109,15 +109,15 @@
   void removeMembers(String... members) throws RestApiException;
 
   /**
-   * List included groups.
+   * Lists the subgroups of this group.
    *
-   * @return included groups.
+   * @return the found subgroups
    * @throws RestApiException
    */
   List<GroupInfo> includedGroups() throws RestApiException;
 
   /**
-   * Add groups to be included in this one.
+   * Adds subgroups to this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
    * @throws RestApiException
@@ -125,7 +125,7 @@
   void addGroups(String... groups) throws RestApiException;
 
   /**
-   * Remove included groups from this one.
+   * Removes subgroups from this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
    * @throws RestApiException
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index f08b3df..32870cb 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gwtorm.server.OrmException;
@@ -38,6 +39,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.index.Term;
@@ -55,7 +57,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LuceneGroupIndex extends AbstractLuceneIndex<AccountGroup.UUID, AccountGroup>
+public class LuceneGroupIndex extends AbstractLuceneIndex<AccountGroup.UUID, InternalGroup>
     implements GroupIndex {
   private static final Logger log = LoggerFactory.getLogger(LuceneGroupIndex.class);
 
@@ -63,7 +65,7 @@
 
   private static final String UUID_SORT_FIELD = sortFieldName(UUID);
 
-  private static Term idTerm(AccountGroup group) {
+  private static Term idTerm(InternalGroup group) {
     return idTerm(group.getGroupUUID());
   }
 
@@ -72,10 +74,10 @@
   }
 
   private final GerritIndexWriterConfig indexWriterConfig;
-  private final QueryBuilder<AccountGroup> queryBuilder;
+  private final QueryBuilder<InternalGroup> queryBuilder;
   private final Provider<GroupCache> groupCache;
 
-  private static Directory dir(Schema<AccountGroup> schema, Config cfg, SitePaths sitePaths)
+  private static Directory dir(Schema<?> schema, Config cfg, SitePaths sitePaths)
       throws IOException {
     if (LuceneIndexModule.isInMemoryTest(cfg)) {
       return new RAMDirectory();
@@ -89,7 +91,7 @@
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
       Provider<GroupCache> groupCache,
-      @Assisted Schema<AccountGroup> schema)
+      @Assisted Schema<InternalGroup> schema)
       throws IOException {
     super(
         schema,
@@ -106,7 +108,7 @@
   }
 
   @Override
-  public void replace(AccountGroup group) throws IOException {
+  public void replace(InternalGroup group) throws IOException {
     try {
       replace(idTerm(group), toDocument(group)).get();
     } catch (ExecutionException | InterruptedException e) {
@@ -124,7 +126,7 @@
   }
 
   @Override
-  public DataSource<AccountGroup> getSource(Predicate<AccountGroup> p, QueryOptions opts)
+  public DataSource<InternalGroup> getSource(Predicate<InternalGroup> p, QueryOptions opts)
       throws QueryParseException {
     return new QuerySource(
         opts,
@@ -132,7 +134,7 @@
         new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false)));
   }
 
-  private class QuerySource implements DataSource<AccountGroup> {
+  private class QuerySource implements DataSource<InternalGroup> {
     private final QueryOptions opts;
     private final Query query;
     private final Sort sort;
@@ -149,27 +151,28 @@
     }
 
     @Override
-    public ResultSet<AccountGroup> read() throws OrmException {
+    public ResultSet<InternalGroup> read() throws OrmException {
       IndexSearcher searcher = null;
       try {
         searcher = acquire();
         int realLimit = opts.start() + opts.limit();
         TopFieldDocs docs = searcher.search(query, realLimit, sort);
-        List<AccountGroup> result = new ArrayList<>(docs.scoreDocs.length);
+        List<InternalGroup> result = new ArrayList<>(docs.scoreDocs.length);
         for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
           Document doc = searcher.doc(sd.doc, IndexUtils.groupFields(opts));
-          result.add(toAccountGroup(doc));
+          Optional<InternalGroup> internalGroup = toInternalGroup(doc);
+          internalGroup.ifPresent(result::add);
         }
-        final List<AccountGroup> r = Collections.unmodifiableList(result);
-        return new ResultSet<AccountGroup>() {
+        final List<InternalGroup> r = Collections.unmodifiableList(result);
+        return new ResultSet<InternalGroup>() {
           @Override
-          public Iterator<AccountGroup> iterator() {
+          public Iterator<InternalGroup> iterator() {
             return r.iterator();
           }
 
           @Override
-          public List<AccountGroup> toList() {
+          public List<InternalGroup> toList() {
             return r;
           }
 
@@ -192,7 +195,7 @@
     }
   }
 
-  private AccountGroup toAccountGroup(Document doc) {
+  private Optional<InternalGroup> toInternalGroup(Document doc) {
     AccountGroup.UUID uuid = new AccountGroup.UUID(doc.getField(UUID.getName()).stringValue());
     // Use the GroupCache rather than depending on any stored fields in the
     // document (of which there shouldn't be any).
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index 8e30a24..82f1559 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import java.io.IOException;
+import java.util.Optional;
 
 /** Tracks group objects in memory for efficient access. */
 public interface GroupCache {
@@ -26,11 +27,13 @@
   AccountGroup get(AccountGroup.NameKey name);
 
   /**
-   * Lookup a group definition by its UUID. The returned definition may be null if the group has
-   * been deleted and the UUID reference is stale, or was copied from another server.
+   * Looks up an internal group by its UUID.
+   *
+   * @param groupUuid the UUID of the internal group
+   * @return an {@code Optional} of the internal group, or an empty {@code Optional} if no internal
+   *     group with this UUID exists on this server or an error occurred during lookup
    */
-  @Nullable
-  AccountGroup get(AccountGroup.UUID uuid);
+  Optional<InternalGroup> get(AccountGroup.UUID groupUuid);
 
   /** @return sorted list of groups. */
   ImmutableList<AccountGroup> all();
@@ -38,7 +41,8 @@
   /** Notify the cache that a new group was constructed. */
   void onCreateGroup(AccountGroup.NameKey newGroupName) throws IOException;
 
-  void evict(AccountGroup group) throws IOException;
+  void evict(AccountGroup.UUID groupUuid, AccountGroup.Id groupId, AccountGroup.NameKey groupName)
+      throws IOException;
 
   void evictAfterRename(AccountGroup.NameKey oldName, AccountGroup.NameKey newName)
       throws IOException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index dc977cb..2901501 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -15,15 +15,19 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.group.Groups;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -58,7 +62,7 @@
         cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<AccountGroup>>() {})
             .loader(ByNameLoader.class);
 
-        cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<AccountGroup>>() {})
+        cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
             .loader(ByUUIDLoader.class);
 
         bind(GroupCacheImpl.class);
@@ -69,7 +73,7 @@
 
   private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
   private final LoadingCache<String, Optional<AccountGroup>> byName;
-  private final LoadingCache<String, Optional<AccountGroup>> byUUID;
+  private final LoadingCache<String, Optional<InternalGroup>> byUUID;
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<GroupIndexer> indexer;
   private final Groups groups;
@@ -78,7 +82,7 @@
   GroupCacheImpl(
       @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
       @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
-      @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID,
+      @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
       SchemaFactory<ReviewDb> schema,
       Provider<GroupIndexer> indexer,
       Groups groups) {
@@ -102,17 +106,19 @@
   }
 
   @Override
-  public void evict(AccountGroup group) throws IOException {
-    if (group.getId() != null) {
-      byId.invalidate(group.getId());
+  public void evict(
+      AccountGroup.UUID groupUuid, AccountGroup.Id groupId, AccountGroup.NameKey groupName)
+      throws IOException {
+    if (groupId != null) {
+      byId.invalidate(groupId);
     }
-    if (group.getNameKey() != null) {
-      byName.invalidate(group.getNameKey().get());
+    if (groupName != null) {
+      byName.invalidate(groupName.get());
     }
-    if (group.getGroupUUID() != null) {
-      byUUID.invalidate(group.getGroupUUID().get());
+    if (groupUuid != null) {
+      byUUID.invalidate(groupUuid.get());
     }
-    indexer.get().index(group.getGroupUUID());
+    indexer.get().index(groupUuid);
   }
 
   @Override
@@ -135,21 +141,22 @@
     try {
       return byName.get(name.get()).orElse(null);
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
+      log.warn(String.format("Cannot look up group %s by name", name.get()), e);
       return null;
     }
   }
 
   @Override
-  public AccountGroup get(AccountGroup.UUID uuid) {
-    if (uuid == null) {
-      return null;
+  public Optional<InternalGroup> get(AccountGroup.UUID groupUuid) {
+    if (groupUuid == null) {
+      return Optional.empty();
     }
+
     try {
-      return byUUID.get(uuid.get()).orElse(null);
+      return byUUID.get(groupUuid.get());
     } catch (ExecutionException e) {
-      log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
-      return null;
+      log.warn(String.format("Cannot look up group %s by uuid", groupUuid.get()), e);
+      return Optional.empty();
     }
   }
 
@@ -210,7 +217,7 @@
     }
   }
 
-  static class ByUUIDLoader extends CacheLoader<String, Optional<AccountGroup>> {
+  static class ByUUIDLoader extends CacheLoader<String, Optional<InternalGroup>> {
     private final SchemaFactory<ReviewDb> schema;
     private final Groups groups;
 
@@ -221,9 +228,20 @@
     }
 
     @Override
-    public Optional<AccountGroup> load(String uuid) throws Exception {
+    public Optional<InternalGroup> load(String uuid) throws Exception {
       try (ReviewDb db = schema.open()) {
-        return groups.getGroup(db, new AccountGroup.UUID(uuid));
+        AccountGroup.UUID groupUuid = new AccountGroup.UUID(uuid);
+        Optional<AccountGroup> accountGroup = groups.getGroup(db, groupUuid);
+
+        if (!accountGroup.isPresent()) {
+          return Optional.empty();
+        }
+
+        ImmutableSet<Account.Id> members =
+            groups.getMembers(db, groupUuid).collect(toImmutableSet());
+        ImmutableSet<AccountGroup.UUID> subgroups =
+            groups.getSubgroups(db, groupUuid).collect(toImmutableSet());
+        return accountGroup.map(group -> InternalGroup.create(group, members, subgroups));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
deleted file mode 100644
index 6ba2e5e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed 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.google.gerrit.server.account;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.util.Comparator;
-
-public class GroupComparator implements Comparator<AccountGroup> {
-
-  @Override
-  public int compare(AccountGroup group1, AccountGroup group2) {
-    return group1.getName().compareTo(group2.getName());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
deleted file mode 100644
index 47bba6a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed 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.google.gerrit.server.account;
-
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.group.Groups;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.util.concurrent.Callable;
-
-public class GroupDetailFactory implements Callable<GroupDetail> {
-  public interface Factory {
-    GroupDetailFactory create(AccountGroup.UUID groupUuid);
-  }
-
-  private final ReviewDb db;
-  private final GroupControl.Factory groupControl;
-  private final Groups groups;
-  private final GroupIncludeCache groupIncludeCache;
-
-  private final AccountGroup.UUID groupUuid;
-  private GroupControl control;
-
-  @Inject
-  GroupDetailFactory(
-      ReviewDb db,
-      GroupControl.Factory groupControl,
-      Groups groups,
-      GroupIncludeCache groupIncludeCache,
-      @Assisted AccountGroup.UUID groupUuid) {
-    this.db = db;
-    this.groupControl = groupControl;
-    this.groups = groups;
-    this.groupIncludeCache = groupIncludeCache;
-
-    this.groupUuid = groupUuid;
-  }
-
-  @Override
-  public GroupDetail call() throws OrmException, NoSuchGroupException {
-    control = groupControl.validateFor(groupUuid);
-    ImmutableSet<Account.Id> members = loadMembers();
-    ImmutableSet<AccountGroup.UUID> includes = loadIncludes();
-    return new GroupDetail(members, includes);
-  }
-
-  private ImmutableSet<Account.Id> loadMembers() throws OrmException, NoSuchGroupException {
-    return groups.getMembers(db, groupUuid).filter(control::canSeeMember).collect(toImmutableSet());
-  }
-
-  private ImmutableSet<AccountGroup.UUID> loadIncludes() {
-    if (!control.canSeeGroup()) {
-      return ImmutableSet.of();
-    }
-
-    return ImmutableSet.copyOf(groupIncludeCache.subgroupsOf(groupUuid));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 0d45397..10c002c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -150,7 +150,7 @@
     public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key)
         throws OrmException, NoSuchGroupException {
       try (ReviewDb db = schema.open()) {
-        return groups.getIncludes(db, key).collect(toImmutableList());
+        return groups.getSubgroups(db, key).collect(toImmutableList());
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index bf8ec33..4dc960d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -14,12 +14,16 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.data.GroupDetail;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -29,6 +33,7 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
 
 public class GroupMembers {
@@ -37,20 +42,20 @@
   }
 
   private final GroupCache groupCache;
-  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final GroupControl.Factory groupControlFactory;
   private final AccountCache accountCache;
   private final ProjectControl.GenericFactory projectControl;
   private final CurrentUser currentUser;
 
   @Inject
   GroupMembers(
-      final GroupCache groupCache,
-      final GroupDetailFactory.Factory groupDetailFactory,
-      final AccountCache accountCache,
-      final ProjectControl.GenericFactory projectControl,
-      @Assisted final CurrentUser currentUser) {
+      GroupCache groupCache,
+      GroupControl.Factory groupControlFactory,
+      AccountCache accountCache,
+      ProjectControl.GenericFactory projectControl,
+      @Assisted CurrentUser currentUser) {
     this.groupCache = groupCache;
-    this.groupDetailFactory = groupDetailFactory;
+    this.groupControlFactory = groupControlFactory;
     this.accountCache = accountCache;
     this.projectControl = projectControl;
     this.currentUser = currentUser;
@@ -69,9 +74,9 @@
     if (SystemGroupBackend.PROJECT_OWNERS.equals(groupUUID)) {
       return getProjectOwners(project, seen);
     }
-    AccountGroup group = groupCache.get(groupUUID);
-    if (group != null) {
-      return getGroupMembers(group, project, seen);
+    Optional<InternalGroup> group = groupCache.get(groupUUID);
+    if (group.isPresent()) {
+      return getGroupMembers(group.get(), project, seen);
     }
     return Collections.emptySet();
   }
@@ -96,22 +101,29 @@
   }
 
   private Set<Account> getGroupMembers(
-      final AccountGroup group, Project.NameKey project, Set<AccountGroup.UUID> seen)
+      InternalGroup group, Project.NameKey project, Set<AccountGroup.UUID> seen)
       throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException {
     seen.add(group.getGroupUUID());
-    final GroupDetail groupDetail = groupDetailFactory.create(group.getGroupUUID()).call();
+    GroupControl groupControl = groupControlFactory.controlFor(new InternalGroupDescription(group));
 
-    final Set<Account> members = new HashSet<>();
-    for (Account.Id memberId : groupDetail.getMembers()) {
-      members.add(accountCache.get(memberId).getAccount());
-    }
+    Set<Account> directMembers =
+        group
+            .getMembers()
+            .stream()
+            .filter(groupControl::canSeeMember)
+            .map(accountCache::get)
+            .map(AccountState::getAccount)
+            .collect(toImmutableSet());
 
-    for (AccountGroup.UUID groupIncludeUuid : groupDetail.getIncludes()) {
-      AccountGroup includedGroup = groupCache.get(groupIncludeUuid);
-      if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
-        members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
+    Set<Account> indirectMembers = new HashSet<>();
+    if (groupControl.canSeeGroup()) {
+      for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
+        if (!seen.contains(subgroupUuid)) {
+          indirectMembers.addAll(listAccounts(subgroupUuid, project, seen));
+        }
       }
     }
-    return members;
+
+    return Sets.union(directMembers, indirectMembers);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 165cbb6..ae28e1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -19,11 +19,13 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -91,11 +93,11 @@
         }
 
         memberOf.put(id, false);
-        AccountGroup group = groupCache.get(id);
-        if (group == null) {
+        Optional<InternalGroup> group = groupCache.get(id);
+        if (!group.isPresent()) {
           continue;
         }
-        if (search(includeCache.subgroupsOf(id))) {
+        if (search(group.get().getSubgroups())) {
           memberOf.put(id, true);
           return true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index 0721212..e471d57c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -17,10 +17,10 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -56,11 +56,7 @@
       return null;
     }
 
-    AccountGroup g = groupCache.get(uuid);
-    if (g == null) {
-      return null;
-    }
-    return GroupDescriptions.forAccountGroup(g);
+    return groupCache.get(uuid).map(InternalGroupDescription::new).orElse(null);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index 84f4535..42213f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -22,10 +22,10 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.group.AddIncludedGroups;
 import com.google.gerrit.server.group.AddMembers;
-import com.google.gerrit.server.group.DeleteIncludedGroups;
+import com.google.gerrit.server.group.AddSubgroups;
 import com.google.gerrit.server.group.DeleteMembers;
+import com.google.gerrit.server.group.DeleteSubgroups;
 import com.google.gerrit.server.group.GetAuditLog;
 import com.google.gerrit.server.group.GetDescription;
 import com.google.gerrit.server.group.GetDetail;
@@ -35,8 +35,8 @@
 import com.google.gerrit.server.group.GetOwner;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.Index;
-import com.google.gerrit.server.group.ListIncludedGroups;
 import com.google.gerrit.server.group.ListMembers;
+import com.google.gerrit.server.group.ListSubgroups;
 import com.google.gerrit.server.group.PutDescription;
 import com.google.gerrit.server.group.PutName;
 import com.google.gerrit.server.group.PutOptions;
@@ -64,9 +64,9 @@
   private final ListMembers listMembers;
   private final AddMembers addMembers;
   private final DeleteMembers deleteMembers;
-  private final ListIncludedGroups listGroups;
-  private final AddIncludedGroups addGroups;
-  private final DeleteIncludedGroups deleteGroups;
+  private final ListSubgroups listSubgroups;
+  private final AddSubgroups addSubgroups;
+  private final DeleteSubgroups deleteSubgroups;
   private final GetAuditLog getAuditLog;
   private final GroupResource rsrc;
   private final Index index;
@@ -86,9 +86,9 @@
       ListMembers listMembers,
       AddMembers addMembers,
       DeleteMembers deleteMembers,
-      ListIncludedGroups listGroups,
-      AddIncludedGroups addGroups,
-      DeleteIncludedGroups deleteGroups,
+      ListSubgroups listSubgroups,
+      AddSubgroups addSubgroups,
+      DeleteSubgroups deleteSubgroups,
       GetAuditLog getAuditLog,
       Index index,
       @Assisted GroupResource rsrc) {
@@ -105,9 +105,9 @@
     this.listMembers = listMembers;
     this.addMembers = addMembers;
     this.deleteMembers = deleteMembers;
-    this.listGroups = listGroups;
-    this.addGroups = addGroups;
-    this.deleteGroups = deleteGroups;
+    this.listSubgroups = listSubgroups;
+    this.addSubgroups = addSubgroups;
+    this.deleteSubgroups = deleteSubgroups;
     this.getAuditLog = getAuditLog;
     this.index = index;
     this.rsrc = rsrc;
@@ -233,27 +233,27 @@
   @Override
   public List<GroupInfo> includedGroups() throws RestApiException {
     try {
-      return listGroups.apply(rsrc);
+      return listSubgroups.apply(rsrc);
     } catch (Exception e) {
-      throw asRestApiException("Cannot list included groups", e);
+      throw asRestApiException("Cannot list subgroups", e);
     }
   }
 
   @Override
   public void addGroups(String... groups) throws RestApiException {
     try {
-      addGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
+      addSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(Arrays.asList(groups)));
     } catch (Exception e) {
-      throw asRestApiException("Cannot add group members", e);
+      throw asRestApiException("Cannot add subgroups", e);
     }
   }
 
   @Override
   public void removeGroups(String... groups) throws RestApiException {
     try {
-      deleteGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups)));
+      deleteSubgroups.apply(rsrc, AddSubgroups.Input.fromGroups(Arrays.asList(groups)));
     } catch (Exception e) {
-      throw asRestApiException("Cannot remove group members", e);
+      throw asRestApiException("Cannot remove subgroups", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 6be83b3..08abfbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -90,7 +90,6 @@
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
@@ -250,7 +249,6 @@
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeJson.AssistedFactory.class);
     factory(CreateChangeSender.Factory.class);
-    factory(GroupDetailFactory.Factory.class);
     factory(GroupMembers.Factory.class);
     factory(EmailMerge.Factory.class);
     factory(MergedSender.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddSubgroups.java
similarity index 76%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/AddSubgroups.java
index 27692c8..2ce168f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddSubgroups.java
@@ -29,18 +29,19 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.group.AddIncludedGroups.Input;
+import com.google.gerrit.server.group.AddSubgroups.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 @Singleton
-public class AddIncludedGroups implements RestModifyView<GroupResource, Input> {
+public class AddSubgroups implements RestModifyView<GroupResource, Input> {
   public static class Input {
     @DefaultInput String _oneGroup;
 
@@ -72,7 +73,7 @@
   private final GroupJson json;
 
   @Inject
-  public AddIncludedGroups(
+  public AddSubgroups(
       GroupsCollection groupsCollection,
       Provider<ReviewDb> db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
@@ -86,7 +87,7 @@
   @Override
   public List<GroupInfo> apply(GroupResource resource, Input input)
       throws MethodNotAllowedException, AuthException, UnprocessableEntityException, OrmException,
-          ResourceNotFoundException {
+          ResourceNotFoundException, IOException {
     GroupDescription.Internal group =
         resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
@@ -97,40 +98,41 @@
     }
 
     List<GroupInfo> result = new ArrayList<>();
-    Set<AccountGroup.UUID> includedGroupUuids = new HashSet<>();
-    for (String includedGroup : input.groups) {
-      GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      includedGroupUuids.add(d.getGroupUUID());
-      result.add(json.format(d));
+    Set<AccountGroup.UUID> subgroupUuids = new HashSet<>();
+    for (String subgroupIdentifier : input.groups) {
+      GroupDescription.Basic subgroup = groupsCollection.parse(subgroupIdentifier);
+      subgroupUuids.add(subgroup.getGroupUUID());
+      result.add(json.format(subgroup));
     }
 
     AccountGroup.UUID groupUuid = group.getGroupUUID();
     try {
-      groupsUpdateProvider.get().addIncludedGroups(db.get(), groupUuid, includedGroupUuids);
+      groupsUpdateProvider.get().addSubgroups(db.get(), groupUuid, subgroupUuids);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
     }
     return result;
   }
 
-  static class PutIncludedGroup implements RestModifyView<GroupResource, PutIncludedGroup.Input> {
+  static class PutSubgroup implements RestModifyView<GroupResource, PutSubgroup.Input> {
     static class Input {}
 
-    private final AddIncludedGroups put;
+    private final AddSubgroups addSubgroups;
     private final String id;
 
-    PutIncludedGroup(AddIncludedGroups put, String id) {
-      this.put = put;
+    PutSubgroup(AddSubgroups addSubgroups, String id) {
+      this.addSubgroups = addSubgroups;
       this.id = id;
     }
 
     @Override
     public GroupInfo apply(GroupResource resource, Input input)
-        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException {
-      AddIncludedGroups.Input in = new AddIncludedGroups.Input();
+        throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException,
+            IOException {
+      AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(id);
       try {
-        List<GroupInfo> list = put.apply(resource, in);
+        List<GroupInfo> list = addSubgroups.apply(resource, in);
         if (list.size() == 1) {
           return list.get(0);
         }
@@ -142,18 +144,16 @@
   }
 
   @Singleton
-  static class UpdateIncludedGroup
-      implements RestModifyView<IncludedGroupResource, PutIncludedGroup.Input> {
-    private final Provider<GetIncludedGroup> get;
+  static class UpdateSubgroup implements RestModifyView<SubgroupResource, PutSubgroup.Input> {
+    private final Provider<GetSubgroup> get;
 
     @Inject
-    UpdateIncludedGroup(Provider<GetIncludedGroup> get) {
+    UpdateSubgroup(Provider<GetSubgroup> get) {
       this.get = get;
     }
 
     @Override
-    public GroupInfo apply(IncludedGroupResource resource, PutIncludedGroup.Input input)
-        throws OrmException {
+    public GroupInfo apply(SubgroupResource resource, PutSubgroup.Input input) throws OrmException {
       // Do nothing, the group is already included.
       return get.get().apply(resource);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteSubgroups.java
similarity index 76%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteSubgroups.java
index 64618c1..14df51b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteSubgroups.java
@@ -26,22 +26,23 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.group.AddIncludedGroups.Input;
+import com.google.gerrit.server.group.AddSubgroups.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
 
 @Singleton
-public class DeleteIncludedGroups implements RestModifyView<GroupResource, Input> {
+public class DeleteSubgroups implements RestModifyView<GroupResource, Input> {
   private final GroupsCollection groupsCollection;
   private final Provider<ReviewDb> db;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
 
   @Inject
-  DeleteIncludedGroups(
+  DeleteSubgroups(
       GroupsCollection groupsCollection,
       Provider<ReviewDb> db,
       @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
@@ -53,7 +54,7 @@
   @Override
   public Response<?> apply(GroupResource resource, Input input)
       throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-          ResourceNotFoundException {
+          ResourceNotFoundException, IOException {
     GroupDescription.Internal internalGroup =
         resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
     input = Input.init(input);
@@ -64,15 +65,15 @@
           String.format("Cannot delete groups from group %s", internalGroup.getName()));
     }
 
-    Set<AccountGroup.UUID> internalGroupsToRemove = new HashSet<>();
-    for (String includedGroup : input.groups) {
-      GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      internalGroupsToRemove.add(d.getGroupUUID());
+    Set<AccountGroup.UUID> subgroupsToRemove = new HashSet<>();
+    for (String subgroupIdentifier : input.groups) {
+      GroupDescription.Basic subgroup = groupsCollection.parse(subgroupIdentifier);
+      subgroupsToRemove.add(subgroup.getGroupUUID());
     }
 
     AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
     try {
-      groupsUpdateProvider.get().deleteIncludedGroups(db.get(), groupUuid, internalGroupsToRemove);
+      groupsUpdateProvider.get().removeSubgroups(db.get(), groupUuid, subgroupsToRemove);
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(String.format("Group %s not found", groupUuid));
     }
@@ -81,22 +82,21 @@
   }
 
   @Singleton
-  static class DeleteIncludedGroup
-      implements RestModifyView<IncludedGroupResource, DeleteIncludedGroup.Input> {
+  static class DeleteSubgroup implements RestModifyView<SubgroupResource, DeleteSubgroup.Input> {
     static class Input {}
 
-    private final Provider<DeleteIncludedGroups> delete;
+    private final Provider<DeleteSubgroups> delete;
 
     @Inject
-    DeleteIncludedGroup(Provider<DeleteIncludedGroups> delete) {
+    DeleteSubgroup(Provider<DeleteSubgroups> delete) {
       this.delete = delete;
     }
 
     @Override
-    public Response<?> apply(IncludedGroupResource resource, Input input)
+    public Response<?> apply(SubgroupResource resource, Input input)
         throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException,
-            ResourceNotFoundException {
-      AddIncludedGroups.Input in = new AddIncludedGroups.Input();
+            ResourceNotFoundException, IOException {
+      AddSubgroups.Input in = new AddSubgroups.Input();
       in.groups = ImmutableList.of(resource.getMember().get());
       return delete.get().apply(resource, in);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
index 4ea7041..8c94f65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.group;
 
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -38,6 +37,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 
 @Singleton
 public class GetAuditLog implements RestReadView<GroupResource> {
@@ -94,10 +94,10 @@
     for (AccountGroupByIdAud auditEvent :
         db.get().accountGroupByIdAud().byGroup(group.getId()).toList()) {
       AccountGroup.UUID includedGroupUUID = auditEvent.getKey().getIncludeUUID();
-      AccountGroup includedGroup = groupCache.get(includedGroupUUID);
+      Optional<InternalGroup> includedGroup = groupCache.get(includedGroupUUID);
       GroupInfo member;
-      if (includedGroup != null) {
-        member = groupJson.format(GroupDescriptions.forAccountGroup(includedGroup));
+      if (includedGroup.isPresent()) {
+        member = groupJson.format(new InternalGroupDescription(includedGroup.get()));
       } else {
         GroupDescription.Basic groupDescription = groupBackend.get(includedGroupUUID);
         member = new GroupInfo();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetSubgroup.java
similarity index 84%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/GetSubgroup.java
index 4cf0cb2..a710188 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetSubgroup.java
@@ -21,16 +21,16 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class GetIncludedGroup implements RestReadView<IncludedGroupResource> {
+public class GetSubgroup implements RestReadView<SubgroupResource> {
   private final GroupJson json;
 
   @Inject
-  GetIncludedGroup(GroupJson json) {
+  GetSubgroup(GroupJson json) {
     this.json = json;
   }
 
   @Override
-  public GroupInfo apply(IncludedGroupResource rsrc) throws OrmException {
+  public GroupInfo apply(SubgroupResource rsrc) throws OrmException {
     return json.format(rsrc.getMemberDescription());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index 639ee55..85be5c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -45,7 +45,7 @@
   private final GroupBackend groupBackend;
   private final GroupControl.Factory groupControlFactory;
   private final Provider<ListMembers> listMembers;
-  private final Provider<ListIncludedGroups> listIncludes;
+  private final Provider<ListSubgroups> listSubgroups;
   private EnumSet<ListGroupsOption> options;
 
   @Inject
@@ -53,11 +53,11 @@
       GroupBackend groupBackend,
       GroupControl.Factory groupControlFactory,
       Provider<ListMembers> listMembers,
-      Provider<ListIncludedGroups> listIncludes) {
+      Provider<ListSubgroups> listSubgroups) {
     this.groupBackend = groupBackend;
     this.groupControlFactory = groupControlFactory;
     this.listMembers = listMembers;
-    this.listIncludes = listIncludes;
+    this.listSubgroups = listSubgroups;
 
     options = EnumSet.noneOf(ListGroupsOption.class);
   }
@@ -74,7 +74,7 @@
 
   public GroupInfo format(GroupResource rsrc) throws OrmException {
     GroupInfo info = init(rsrc.getGroup());
-    initMembersAndIncludes(rsrc, info);
+    initMembersAndSubgroups(rsrc, info);
     return info;
   }
 
@@ -82,7 +82,7 @@
     GroupInfo info = init(group);
     if (options.contains(MEMBERS) || options.contains(INCLUDES)) {
       GroupResource rsrc = new GroupResource(groupControlFactory.controlFor(group));
-      initMembersAndIncludes(rsrc, info);
+      initMembersAndSubgroups(rsrc, info);
     }
     return info;
   }
@@ -116,7 +116,8 @@
     return group instanceof GroupDescription.Internal;
   }
 
-  private GroupInfo initMembersAndIncludes(GroupResource rsrc, GroupInfo info) throws OrmException {
+  private GroupInfo initMembersAndSubgroups(GroupResource rsrc, GroupInfo info)
+      throws OrmException {
     if (!rsrc.isInternalGroup()) {
       return info;
     }
@@ -126,7 +127,7 @@
       }
 
       if (options.contains(INCLUDES)) {
-        info.includes = listIncludes.get().apply(rsrc);
+        info.includes = listSubgroups.get().apply(rsrc);
       }
       return info;
     } catch (MethodNotAllowedException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
index 9f840ee..b835d22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Groups.java
@@ -145,16 +145,16 @@
    *
    * @param db the {@code ReviewDb} instance to use for lookups
    * @param parentGroupUuid the UUID of the parent group
-   * @param includedGroupUuid the UUID of the subgroup
+   * @param subgroupUuid the UUID of the subgroup
    * @return {@code true} if the group is a subgroup of the other group, or else {@code false}
    * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws NoSuchGroupException if the specified parent group doesn't exist
    */
-  public boolean isIncluded(
-      ReviewDb db, AccountGroup.UUID parentGroupUuid, AccountGroup.UUID includedGroupUuid)
+  public boolean isSubgroup(
+      ReviewDb db, AccountGroup.UUID parentGroupUuid, AccountGroup.UUID subgroupUuid)
       throws OrmException, NoSuchGroupException {
     AccountGroup parentGroup = getExistingGroup(db, parentGroupUuid);
-    AccountGroupById.Key key = new AccountGroupById.Key(parentGroup.getId(), includedGroupUuid);
+    AccountGroupById.Key key = new AccountGroupById.Key(parentGroup.getId(), subgroupUuid);
     return db.accountGroupById().get(key) != null;
   }
 
@@ -191,7 +191,7 @@
    * @throws OrmException if an error occurs while reading from ReviewDb
    * @throws NoSuchGroupException if the specified parent group doesn't exist
    */
-  public Stream<AccountGroup.UUID> getIncludes(ReviewDb db, AccountGroup.UUID groupUuid)
+  public Stream<AccountGroup.UUID> getSubgroups(ReviewDb db, AccountGroup.UUID groupUuid)
       throws OrmException, NoSuchGroupException {
     AccountGroup group = getExistingGroup(db, groupUuid);
     ResultSet<AccountGroupById> accountGroupByIds = db.accountGroupById().byGroup(group.getId());
@@ -226,14 +226,14 @@
    * exist. This method doesn't check whether the parent groups exist.
    *
    * @param db the {@code ReviewDb} instance to use for lookups
-   * @param includedGroupUuid the UUID of the subgroup
+   * @param subgroupUuid the UUID of the subgroup
    * @return a stream of the IDs of the parent groups
    * @throws OrmException if an error occurs while reading from ReviewDb
    */
-  public Stream<AccountGroup.Id> getParentGroups(ReviewDb db, AccountGroup.UUID includedGroupUuid)
+  public Stream<AccountGroup.Id> getParentGroups(ReviewDb db, AccountGroup.UUID subgroupUuid)
       throws OrmException {
     ResultSet<AccountGroupById> accountGroupByIds =
-        db.accountGroupById().byIncludeUUID(includedGroupUuid);
+        db.accountGroupById().byIncludeUUID(subgroupUuid);
     return Streams.stream(accountGroupByIds).map(AccountGroupById::getGroupId);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
index 339a276..d688e4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsUpdate.java
@@ -170,7 +170,7 @@
       ReviewDb db, AccountGroup.UUID groupUuid, Consumer<AccountGroup> groupConsumer)
       throws OrmException, IOException, NoSuchGroupException {
     AccountGroup updatedGroup = updateGroupInDb(db, groupUuid, groupConsumer);
-    groupCache.evict(updatedGroup);
+    groupCache.evict(updatedGroup.getGroupUUID(), updatedGroup.getId(), updatedGroup.getNameKey());
   }
 
   @VisibleForTesting
@@ -221,7 +221,7 @@
 
     db.accountGroupNames().deleteKeys(ImmutableList.of(oldName));
 
-    groupCache.evict(group);
+    groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
     groupCache.evictAfterRename(oldName, newName);
 
     @SuppressWarnings("unused")
@@ -259,7 +259,7 @@
    * @param groupUuid the UUID of the group
    * @param accountIds a set of IDs of accounts to add
    * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
-   * @throws IOException if the cache entry of one of the new members couldn't be invalidated
+   * @throws IOException if the group or one of the new members couldn't be indexed
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
   public void addGroupMembers(ReviewDb db, AccountGroup.UUID groupUuid, Set<Account.Id> accountIds)
@@ -293,6 +293,7 @@
       auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), newMembers);
     }
     db.accountGroupMembers().insert(newMembers);
+    groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
     for (AccountGroupMember newMember : newMembers) {
       accountCache.evict(newMember.getAccountId());
     }
@@ -306,7 +307,7 @@
    * @param groupUuid the UUID of the group
    * @param accountIds a set of IDs of accounts to remove
    * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
-   * @throws IOException if the cache entry of one of the removed members couldn't be invalidated
+   * @throws IOException if the group or one of the removed members couldn't be indexed
    * @throws NoSuchGroupException if the specified group doesn't exist
    */
   public void removeGroupMembers(
@@ -331,6 +332,7 @@
       auditService.dispatchDeleteAccountsFromGroup(currentUser.getAccountId(), membersToRemove);
     }
     db.accountGroupMembers().delete(membersToRemove);
+    groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
     for (AccountGroupMember member : membersToRemove) {
       accountCache.evict(member.getAccountId());
     }
@@ -347,33 +349,35 @@
    *
    * @param db the {@code ReviewDb} instance to update
    * @param parentGroupUuid the UUID of the parent group
-   * @param includedGroupUuids a set of IDs of the groups to add as subgroups
+   * @param subgroupUuids a set of IDs of the groups to add as subgroups
    * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
+   * @throws IOException if the parent group couldn't be indexed
    * @throws NoSuchGroupException if the specified parent group doesn't exist
    */
-  public void addIncludedGroups(
-      ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> includedGroupUuids)
-      throws OrmException, NoSuchGroupException {
+  public void addSubgroups(
+      ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids)
+      throws OrmException, NoSuchGroupException, IOException {
     AccountGroup parentGroup = groups.getExistingGroup(db, parentGroupUuid);
     AccountGroup.Id parentGroupId = parentGroup.getId();
-    Set<AccountGroupById> newIncludedGroups = new HashSet<>();
-    for (AccountGroup.UUID includedGroupUuid : includedGroupUuids) {
-      boolean isIncluded = groups.isIncluded(db, parentGroupUuid, includedGroupUuid);
-      if (!isIncluded) {
+    Set<AccountGroupById> newSubgroups = new HashSet<>();
+    for (AccountGroup.UUID includedGroupUuid : subgroupUuids) {
+      boolean isSubgroup = groups.isSubgroup(db, parentGroupUuid, includedGroupUuid);
+      if (!isSubgroup) {
         AccountGroupById.Key key = new AccountGroupById.Key(parentGroupId, includedGroupUuid);
-        newIncludedGroups.add(new AccountGroupById(key));
+        newSubgroups.add(new AccountGroupById(key));
       }
     }
 
-    if (newIncludedGroups.isEmpty()) {
+    if (newSubgroups.isEmpty()) {
       return;
     }
 
     if (currentUser != null) {
-      auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), newIncludedGroups);
+      auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), newSubgroups);
     }
-    db.accountGroupById().insert(newIncludedGroups);
-    for (AccountGroupById newIncludedGroup : newIncludedGroups) {
+    db.accountGroupById().insert(newSubgroups);
+    groupCache.evict(parentGroup.getGroupUUID(), parentGroup.getId(), parentGroup.getNameKey());
+    for (AccountGroupById newIncludedGroup : newSubgroups) {
       groupIncludeCache.evictParentGroupsOf(newIncludedGroup.getIncludeUUID());
     }
     groupIncludeCache.evictSubgroupsOf(parentGroupUuid);
@@ -388,34 +392,35 @@
    *
    * @param db the {@code ReviewDb} instance to update
    * @param parentGroupUuid the UUID of the parent group
-   * @param includedGroupUuids a set of IDs of the subgroups to remove from the parent group
+   * @param subgroupUuids a set of IDs of the subgroups to remove from the parent group
    * @throws OrmException if an error occurs while reading/writing from/to ReviewDb
+   * @throws IOException if the parent group couldn't be indexed
    * @throws NoSuchGroupException if the specified parent group doesn't exist
    */
-  public void deleteIncludedGroups(
-      ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> includedGroupUuids)
-      throws OrmException, NoSuchGroupException {
+  public void removeSubgroups(
+      ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids)
+      throws OrmException, NoSuchGroupException, IOException {
     AccountGroup parentGroup = groups.getExistingGroup(db, parentGroupUuid);
     AccountGroup.Id parentGroupId = parentGroup.getId();
-    Set<AccountGroupById> includedGroupsToRemove = new HashSet<>();
-    for (AccountGroup.UUID includedGroupUuid : includedGroupUuids) {
-      boolean isIncluded = groups.isIncluded(db, parentGroupUuid, includedGroupUuid);
-      if (isIncluded) {
-        AccountGroupById.Key key = new AccountGroupById.Key(parentGroupId, includedGroupUuid);
-        includedGroupsToRemove.add(new AccountGroupById(key));
+    Set<AccountGroupById> subgroupsToRemove = new HashSet<>();
+    for (AccountGroup.UUID subgroupUuid : subgroupUuids) {
+      boolean isSubgroup = groups.isSubgroup(db, parentGroupUuid, subgroupUuid);
+      if (isSubgroup) {
+        AccountGroupById.Key key = new AccountGroupById.Key(parentGroupId, subgroupUuid);
+        subgroupsToRemove.add(new AccountGroupById(key));
       }
     }
 
-    if (includedGroupsToRemove.isEmpty()) {
+    if (subgroupsToRemove.isEmpty()) {
       return;
     }
 
     if (currentUser != null) {
-      auditService.dispatchDeleteGroupsFromGroup(
-          currentUser.getAccountId(), includedGroupsToRemove);
+      auditService.dispatchDeleteGroupsFromGroup(currentUser.getAccountId(), subgroupsToRemove);
     }
-    db.accountGroupById().delete(includedGroupsToRemove);
-    for (AccountGroupById groupToRemove : includedGroupsToRemove) {
+    db.accountGroupById().delete(subgroupsToRemove);
+    groupCache.evict(parentGroup.getGroupUUID(), parentGroup.getId(), parentGroup.getNameKey());
+    for (AccountGroupById groupToRemove : subgroupsToRemove) {
       groupIncludeCache.evictParentGroupsOf(groupToRemove.getIncludeUUID());
     }
     groupIncludeCache.evictSubgroupsOf(parentGroupUuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
index 5d076d9..b61f954 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
@@ -24,6 +24,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Optional;
 
 @Singleton
 public class Index implements RestModifyView<GroupResource, Input> {
@@ -49,9 +50,11 @@
           String.format("External Group Not Allowed: %s", groupUuid.get()));
     }
 
-    AccountGroup accountGroup = groupCache.get(groupUuid);
+    Optional<InternalGroup> group = groupCache.get(groupUuid);
     // evicting the group from the cache, reindexes the group
-    groupCache.evict(accountGroup);
+    if (group.isPresent()) {
+      groupCache.evict(group.get().getGroupUUID(), group.get().getId(), group.get().getNameKey());
+    }
     return Response.none();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java
new file mode 100644
index 0000000..228d86f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroup.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed 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.google.gerrit.server.group;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
+
+@AutoValue
+public abstract class InternalGroup {
+
+  public static InternalGroup create(
+      AccountGroup accountGroup,
+      ImmutableSet<Account.Id> members,
+      ImmutableSet<AccountGroup.UUID> subgroups) {
+    return new AutoValue_InternalGroup(
+        accountGroup.getId(),
+        accountGroup.getNameKey(),
+        accountGroup.getDescription(),
+        accountGroup.getOwnerGroupUUID(),
+        accountGroup.isVisibleToAll(),
+        accountGroup.getGroupUUID(),
+        accountGroup.getCreatedOn(),
+        members,
+        subgroups);
+  }
+
+  public abstract AccountGroup.Id getId();
+
+  public String getName() {
+    return getNameKey().get();
+  }
+
+  public abstract AccountGroup.NameKey getNameKey();
+
+  @Nullable
+  public abstract String getDescription();
+
+  public abstract AccountGroup.UUID getOwnerGroupUUID();
+
+  public abstract boolean isVisibleToAll();
+
+  public abstract AccountGroup.UUID getGroupUUID();
+
+  public abstract Timestamp getCreatedOn();
+
+  public abstract ImmutableSet<Account.Id> getMembers();
+
+  public abstract ImmutableSet<AccountGroup.UUID> getSubgroups();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroupDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroupDescription.java
new file mode 100644
index 0000000..c5df2ff
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/InternalGroupDescription.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed 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.google.gerrit.server.group;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import java.sql.Timestamp;
+
+public class InternalGroupDescription implements GroupDescription.Internal {
+
+  private final InternalGroup internalGroup;
+
+  public InternalGroupDescription(InternalGroup internalGroup) {
+    this.internalGroup = checkNotNull(internalGroup);
+  }
+
+  @Override
+  public AccountGroup.UUID getGroupUUID() {
+    return internalGroup.getGroupUUID();
+  }
+
+  @Override
+  public String getName() {
+    return internalGroup.getName();
+  }
+
+  @Nullable
+  @Override
+  public String getEmailAddress() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getUrl() {
+    return "#" + PageLinks.toGroup(getGroupUUID());
+  }
+
+  @Override
+  public AccountGroup.Id getId() {
+    return internalGroup.getId();
+  }
+
+  @Override
+  @Nullable
+  public String getDescription() {
+    return internalGroup.getDescription();
+  }
+
+  @Override
+  public AccountGroup.UUID getOwnerGroupUUID() {
+    return internalGroup.getOwnerGroupUUID();
+  }
+
+  @Override
+  public boolean isVisibleToAll() {
+    return internalGroup.isVisibleToAll();
+  }
+
+  @Override
+  public Timestamp getCreatedOn() {
+    return internalGroup.getCreatedOn();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index 23c5fee..ff8396b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.group;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GroupDescription;
@@ -35,7 +38,6 @@
 import com.google.gerrit.server.account.GetGroups;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupComparator;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
@@ -43,13 +45,14 @@
 import com.google.inject.Provider;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
@@ -58,6 +61,8 @@
 
 /** List groups visible to the calling user. */
 public class ListGroups implements RestReadView<TopLevelResource> {
+  private static final Comparator<GroupDescription.Internal> GROUP_COMPARATOR =
+      Comparator.comparing(GroupDescription.Basic::getName);
 
   protected final GroupCache groupCache;
 
@@ -266,33 +271,32 @@
 
   private List<GroupInfo> getAllGroups() throws OrmException {
     List<GroupInfo> groupInfos;
-    List<AccountGroup> groupList;
+    List<GroupDescription.Internal> groupList;
     if (!projects.isEmpty()) {
-      Map<AccountGroup.UUID, AccountGroup> groups = new HashMap<>();
+      Map<AccountGroup.UUID, GroupDescription.Internal> groups = new HashMap<>();
       for (ProjectControl projectControl : projects) {
         final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
         for (GroupReference groupRef : groupsRefs) {
-          final AccountGroup group = groupCache.get(groupRef.getUUID());
-          if (group != null) {
-            groups.put(group.getGroupUUID(), group);
-          }
+          Optional<InternalGroup> internalGroup = groupCache.get(groupRef.getUUID());
+          internalGroup.ifPresent(
+              group -> groups.put(group.getGroupUUID(), new InternalGroupDescription(group)));
         }
       }
       groupList = filterGroups(groups.values());
     } else {
-      groupList = filterGroups(groupCache.all());
+      groupList = filterGroups(getAllExistingInternalGroups());
     }
     groupInfos = Lists.newArrayListWithCapacity(groupList.size());
     int found = 0;
     int foundIndex = 0;
-    for (AccountGroup group : groupList) {
+    for (GroupDescription.Internal group : groupList) {
       if (foundIndex++ < start) {
         continue;
       }
       if (limit > 0 && ++found > limit) {
         break;
       }
-      groupInfos.add(json.addOptions(options).format(GroupDescriptions.forAccountGroup(group)));
+      groupInfos.add(json.addOptions(options).format(group));
     }
     return groupInfos;
   }
@@ -355,7 +359,7 @@
     List<GroupInfo> groups = new ArrayList<>();
     int found = 0;
     int foundIndex = 0;
-    for (AccountGroup g : filterGroups(groupCache.all())) {
+    for (GroupDescription.Internal g : filterGroups(getAllExistingInternalGroups())) {
       GroupControl ctl = groupControlFactory.controlFor(g);
       try {
         if (genericGroupControlFactory.controlFor(user, g.getGroupUUID()).isOwner()) {
@@ -374,10 +378,19 @@
     return groups;
   }
 
-  private List<AccountGroup> filterGroups(Collection<AccountGroup> groups) {
-    List<AccountGroup> filteredGroups = new ArrayList<>(groups.size());
+  private ImmutableList<GroupDescription.Internal> getAllExistingInternalGroups() {
+    return groupCache
+        .all()
+        .stream()
+        .map(GroupDescriptions::forAccountGroup)
+        .collect(toImmutableList());
+  }
+
+  private List<GroupDescription.Internal> filterGroups(
+      Collection<GroupDescription.Internal> groups) {
+    List<GroupDescription.Internal> filteredGroups = new ArrayList<>(groups.size());
     Pattern pattern = Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
-    for (AccountGroup group : groups) {
+    for (GroupDescription.Internal group : groups) {
       if (!Strings.isNullOrEmpty(matchSubstring)) {
         if (!group
             .getName()
@@ -402,7 +415,7 @@
         filteredGroups.add(group);
       }
     }
-    Collections.sort(filteredGroups, new GroupComparator());
+    filteredGroups.sort(GROUP_COMPARATOR);
     return filteredGroups;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index 0f8aa40..af988b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.common.collect.Lists;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -25,20 +26,20 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.api.accounts.AccountInfoComparator;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import java.util.Collections;
-import java.util.HashMap;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import org.kohsuke.args4j.Option;
 
 public class ListMembers implements RestReadView<GroupResource> {
   private final GroupCache groupCache;
-  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final GroupControl.Factory groupControlFactory;
   private final AccountLoader accountLoader;
 
   @Option(name = "--recursive", usage = "to resolve included groups recursively")
@@ -47,10 +48,10 @@
   @Inject
   protected ListMembers(
       GroupCache groupCache,
-      GroupDetailFactory.Factory groupDetailFactory,
+      GroupControl.Factory groupControlFactory,
       AccountLoader.Factory accountLoaderFactory) {
     this.groupCache = groupCache;
-    this.groupDetailFactory = groupDetailFactory;
+    this.groupControlFactory = groupControlFactory;
     this.accountLoader = accountLoaderFactory.create(true);
   }
 
@@ -67,52 +68,40 @@
     return apply(group.getGroupUUID());
   }
 
-  public List<AccountInfo> apply(AccountGroup group) throws OrmException {
-    return apply(group.getGroupUUID());
-  }
-
   public List<AccountInfo> apply(AccountGroup.UUID groupId) throws OrmException {
-    final Map<Account.Id, AccountInfo> members =
-        getMembers(groupId, new HashSet<AccountGroup.UUID>());
-    final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
-    Collections.sort(memberInfos, AccountInfoComparator.ORDER_NULLS_FIRST);
+    Set<Account.Id> members = getMembers(groupId, new HashSet<>());
+    List<AccountInfo> memberInfos = new ArrayList<>(members.size());
+    for (Account.Id member : members) {
+      memberInfos.add(accountLoader.get(member));
+    }
+    accountLoader.fill();
+    memberInfos.sort(AccountInfoComparator.ORDER_NULLS_FIRST);
     return memberInfos;
   }
 
-  private Map<Account.Id, AccountInfo> getMembers(
-      final AccountGroup.UUID groupUUID, HashSet<AccountGroup.UUID> seenGroups)
-      throws OrmException {
+  private Set<Account.Id> getMembers(
+      AccountGroup.UUID groupUUID, HashSet<AccountGroup.UUID> seenGroups) {
     seenGroups.add(groupUUID);
 
-    final Map<Account.Id, AccountInfo> members = new HashMap<>();
-    final AccountGroup group = groupCache.get(groupUUID);
-    if (group == null) {
-      // the included group is an external group and can't be resolved
-      return Collections.emptyMap();
+    Optional<InternalGroup> internalGroup = groupCache.get(groupUUID);
+    if (!internalGroup.isPresent()) {
+      return ImmutableSet.of();
     }
+    InternalGroup group = internalGroup.get();
 
-    final GroupDetail groupDetail;
-    try {
-      groupDetail = groupDetailFactory.create(group.getGroupUUID()).call();
-    } catch (NoSuchGroupException e) {
-      // the included group is not visible
-      return Collections.emptyMap();
-    }
+    GroupControl groupControl = groupControlFactory.controlFor(new InternalGroupDescription(group));
 
-    for (Account.Id member : groupDetail.getMembers()) {
-      if (!members.containsKey(member)) {
-        members.put(member, accountLoader.get(member));
-      }
-    }
+    Set<Account.Id> directMembers =
+        group.getMembers().stream().filter(groupControl::canSeeMember).collect(toImmutableSet());
 
-    if (recursive) {
-      for (AccountGroup.UUID includedGroupUuid : groupDetail.getIncludes()) {
-        if (!seenGroups.contains(includedGroupUuid)) {
-          members.putAll(getMembers(includedGroupUuid, seenGroups));
+    Set<Account.Id> indirectMembers = new HashSet<>();
+    if (recursive && groupControl.canSeeGroup()) {
+      for (AccountGroup.UUID subgroupUuid : group.getSubgroups()) {
+        if (!seenGroups.contains(subgroupUuid)) {
+          indirectMembers.addAll(getMembers(subgroupUuid, seenGroups));
         }
       }
     }
-    accountLoader.fill();
-    return members;
+    return Sets.union(directMembers, indirectMembers);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListSubgroups.java
similarity index 86%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/ListSubgroups.java
index 33d9b57..2000d66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListSubgroups.java
@@ -35,15 +35,15 @@
 import org.slf4j.Logger;
 
 @Singleton
-public class ListIncludedGroups implements RestReadView<GroupResource> {
-  private static final Logger log = org.slf4j.LoggerFactory.getLogger(ListIncludedGroups.class);
+public class ListSubgroups implements RestReadView<GroupResource> {
+  private static final Logger log = org.slf4j.LoggerFactory.getLogger(ListSubgroups.class);
 
   private final GroupControl.Factory controlFactory;
   private final GroupIncludeCache groupIncludeCache;
   private final GroupJson json;
 
   @Inject
-  ListIncludedGroups(
+  ListSubgroups(
       GroupControl.Factory controlFactory, GroupIncludeCache groupIncludeCache, GroupJson json) {
     this.controlFactory = controlFactory;
     this.groupIncludeCache = groupIncludeCache;
@@ -57,19 +57,18 @@
 
     boolean ownerOfParent = rsrc.getControl().isOwner();
     List<GroupInfo> included = new ArrayList<>();
-    Collection<AccountGroup.UUID> includedGroupUuids =
+    Collection<AccountGroup.UUID> subgroupUuids =
         groupIncludeCache.subgroupsOf(group.getGroupUUID());
-    for (AccountGroup.UUID includedGroupUuid : includedGroupUuids) {
+    for (AccountGroup.UUID subgroupUuid : subgroupUuids) {
       try {
-        GroupControl i = controlFactory.controlFor(includedGroupUuid);
+        GroupControl i = controlFactory.controlFor(subgroupUuid);
         if (ownerOfParent || i.isVisible()) {
           included.add(json.format(i.getGroup()));
         }
       } catch (NoSuchGroupException notFound) {
         log.warn(
             String.format(
-                "Group %s no longer available, included into %s",
-                includedGroupUuid, group.getName()));
+                "Group %s no longer available, subgroup of %s", subgroupUuid, group.getName()));
         continue;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
index ef70484..5006914 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -15,18 +15,18 @@
 package com.google.gerrit.server.group;
 
 import static com.google.gerrit.server.group.GroupResource.GROUP_KIND;
-import static com.google.gerrit.server.group.IncludedGroupResource.INCLUDED_GROUP_KIND;
 import static com.google.gerrit.server.group.MemberResource.MEMBER_KIND;
+import static com.google.gerrit.server.group.SubgroupResource.SUBGROUP_KIND;
 
 import com.google.gerrit.audit.GroupMemberAuditListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.group.AddIncludedGroups.UpdateIncludedGroup;
 import com.google.gerrit.server.group.AddMembers.UpdateMember;
-import com.google.gerrit.server.group.DeleteIncludedGroups.DeleteIncludedGroup;
+import com.google.gerrit.server.group.AddSubgroups.UpdateSubgroup;
 import com.google.gerrit.server.group.DeleteMembers.DeleteMember;
+import com.google.gerrit.server.group.DeleteSubgroups.DeleteSubgroup;
 import com.google.inject.Provides;
 
 public class Module extends RestApiModule {
@@ -36,7 +36,7 @@
 
     DynamicMap.mapOf(binder(), GROUP_KIND);
     DynamicMap.mapOf(binder(), MEMBER_KIND);
-    DynamicMap.mapOf(binder(), INCLUDED_GROUP_KIND);
+    DynamicMap.mapOf(binder(), SUBGROUP_KIND);
 
     get(GROUP_KIND).to(GetGroup.class);
     put(GROUP_KIND).to(PutGroup.class);
@@ -45,9 +45,9 @@
     post(GROUP_KIND, "members").to(AddMembers.class);
     post(GROUP_KIND, "members.add").to(AddMembers.class);
     post(GROUP_KIND, "members.delete").to(DeleteMembers.class);
-    post(GROUP_KIND, "groups").to(AddIncludedGroups.class);
-    post(GROUP_KIND, "groups.add").to(AddIncludedGroups.class);
-    post(GROUP_KIND, "groups.delete").to(DeleteIncludedGroups.class);
+    post(GROUP_KIND, "groups").to(AddSubgroups.class);
+    post(GROUP_KIND, "groups.add").to(AddSubgroups.class);
+    post(GROUP_KIND, "groups.delete").to(DeleteSubgroups.class);
     get(GROUP_KIND, "description").to(GetDescription.class);
     put(GROUP_KIND, "description").to(PutDescription.class);
     delete(GROUP_KIND, "description").to(PutDescription.class);
@@ -64,10 +64,10 @@
     put(MEMBER_KIND).to(UpdateMember.class);
     delete(MEMBER_KIND).to(DeleteMember.class);
 
-    child(GROUP_KIND, "groups").to(IncludedGroupsCollection.class);
-    get(INCLUDED_GROUP_KIND).to(GetIncludedGroup.class);
-    put(INCLUDED_GROUP_KIND).to(UpdateIncludedGroup.class);
-    delete(INCLUDED_GROUP_KIND).to(DeleteIncludedGroup.class);
+    child(GROUP_KIND, "groups").to(SubgroupsCollection.class);
+    get(SUBGROUP_KIND).to(GetSubgroup.class);
+    put(SUBGROUP_KIND).to(UpdateSubgroup.class);
+    delete(SUBGROUP_KIND).to(DeleteSubgroup.class);
 
     factory(CreateGroup.Factory.class);
     factory(GroupsUpdate.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java
index 32ea4e4..89912b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java
@@ -16,7 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -25,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.query.group.GroupQueryBuilder;
@@ -123,13 +121,13 @@
     }
 
     try {
-      QueryResult<AccountGroup> result = queryProcessor.query(queryBuilder.parse(query));
-      List<AccountGroup> groups = result.entities();
+      QueryResult<InternalGroup> result = queryProcessor.query(queryBuilder.parse(query));
+      List<InternalGroup> groups = result.entities();
 
       ArrayList<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groups.size());
       json.addOptions(options);
-      for (AccountGroup group : groups) {
-        groupInfos.add(json.format(GroupDescriptions.forAccountGroup(group)));
+      for (InternalGroup group : groups) {
+        groupInfos.add(json.format(new InternalGroupDescription(group)));
       }
       if (!groupInfos.isEmpty() && result.more()) {
         groupInfos.get(groupInfos.size() - 1)._moreGroups = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupResource.java
similarity index 79%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupResource.java
index 467de4c..50c769d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupResource.java
@@ -19,13 +19,13 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.TypeLiteral;
 
-public class IncludedGroupResource extends GroupResource {
-  public static final TypeLiteral<RestView<IncludedGroupResource>> INCLUDED_GROUP_KIND =
-      new TypeLiteral<RestView<IncludedGroupResource>>() {};
+public class SubgroupResource extends GroupResource {
+  public static final TypeLiteral<RestView<SubgroupResource>> SUBGROUP_KIND =
+      new TypeLiteral<RestView<SubgroupResource>>() {};
 
   private final GroupDescription.Basic member;
 
-  public IncludedGroupResource(GroupResource group, GroupDescription.Basic member) {
+  public SubgroupResource(GroupResource group, GroupDescription.Basic member) {
     super(group);
     this.member = member;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupsCollection.java
similarity index 72%
rename from gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupsCollection.java
index d48acee..e2aacd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SubgroupsCollection.java
@@ -26,36 +26,36 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.group.AddIncludedGroups.PutIncludedGroup;
+import com.google.gerrit.server.group.AddSubgroups.PutSubgroup;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
-public class IncludedGroupsCollection
-    implements ChildCollection<GroupResource, IncludedGroupResource>, AcceptsCreate<GroupResource> {
-  private final DynamicMap<RestView<IncludedGroupResource>> views;
-  private final ListIncludedGroups list;
+public class SubgroupsCollection
+    implements ChildCollection<GroupResource, SubgroupResource>, AcceptsCreate<GroupResource> {
+  private final DynamicMap<RestView<SubgroupResource>> views;
+  private final ListSubgroups list;
   private final GroupsCollection groupsCollection;
   private final Provider<ReviewDb> dbProvider;
   private final Groups groups;
-  private final AddIncludedGroups put;
+  private final AddSubgroups addSubgroups;
 
   @Inject
-  IncludedGroupsCollection(
-      DynamicMap<RestView<IncludedGroupResource>> views,
-      ListIncludedGroups list,
+  SubgroupsCollection(
+      DynamicMap<RestView<SubgroupResource>> views,
+      ListSubgroups list,
       GroupsCollection groupsCollection,
       Provider<ReviewDb> dbProvider,
       Groups groups,
-      AddIncludedGroups put) {
+      AddSubgroups addSubgroups) {
     this.views = views;
     this.list = list;
     this.groupsCollection = groupsCollection;
     this.dbProvider = dbProvider;
     this.groups = groups;
-    this.put = put;
+    this.addSubgroups = addSubgroups;
   }
 
   @Override
@@ -64,23 +64,23 @@
   }
 
   @Override
-  public IncludedGroupResource parse(GroupResource resource, IdString id)
+  public SubgroupResource parse(GroupResource resource, IdString id)
       throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException {
     GroupDescription.Internal parent =
         resource.asInternalGroup().orElseThrow(MethodNotAllowedException::new);
 
     GroupDescription.Basic member =
         groupsCollection.parse(TopLevelResource.INSTANCE, id).getGroup();
-    if (resource.getControl().canSeeGroup() && isMember(parent, member)) {
-      return new IncludedGroupResource(resource, member);
+    if (resource.getControl().canSeeGroup() && isSubgroup(parent, member)) {
+      return new SubgroupResource(resource, member);
     }
     throw new ResourceNotFoundException(id);
   }
 
-  private boolean isMember(GroupDescription.Internal parent, GroupDescription.Basic member)
+  private boolean isSubgroup(GroupDescription.Internal parent, GroupDescription.Basic member)
       throws OrmException, ResourceNotFoundException {
     try {
-      return groups.isIncluded(dbProvider.get(), parent.getGroupUUID(), member.getGroupUUID());
+      return groups.isSubgroup(dbProvider.get(), parent.getGroupUUID(), member.getGroupUUID());
     } catch (NoSuchGroupException e) {
       throw new ResourceNotFoundException(
           String.format("Group %s not found", parent.getGroupUUID()));
@@ -89,12 +89,12 @@
 
   @SuppressWarnings("unchecked")
   @Override
-  public PutIncludedGroup create(GroupResource group, IdString id) {
-    return new PutIncludedGroup(put, id.get());
+  public PutSubgroup create(GroupResource group, IdString id) {
+    return new PutSubgroup(addSubgroups, id.get());
   }
 
   @Override
-  public DynamicMap<RestView<IncludedGroupResource>> views() {
+  public DynamicMap<RestView<SubgroupResource>> views() {
     return views;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
index 51ef634..481726b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
@@ -17,8 +17,8 @@
 import com.google.gerrit.index.Index;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.Schema;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.DummyChangeIndex;
@@ -43,7 +43,7 @@
 
   private static class DummyGroupIndexFactory implements GroupIndex.Factory {
     @Override
-    public GroupIndex create(Schema<AccountGroup> schema) {
+    public GroupIndex create(Schema<InternalGroup> schema) {
       throw new UnsupportedOperationException();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 7c4074a..2b59675 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.Groups;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -34,6 +35,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -43,7 +45,7 @@
 import org.slf4j.LoggerFactory;
 
 @Singleton
-public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, AccountGroup, GroupIndex> {
+public class AllGroupsIndexer extends SiteIndexer<AccountGroup.UUID, InternalGroup, GroupIndex> {
   private static final Logger log = LoggerFactory.getLogger(AllGroupsIndexer.class);
 
   private final SchemaFactory<ReviewDb> schemaFactory;
@@ -92,11 +94,17 @@
           executor.submit(
               () -> {
                 try {
-                  AccountGroup oldGroup = groupCache.get(uuid);
-                  if (oldGroup != null) {
-                    groupCache.evict(oldGroup);
+                  Optional<InternalGroup> oldGroup = groupCache.get(uuid);
+                  if (oldGroup.isPresent()) {
+                    InternalGroup group = oldGroup.get();
+                    groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
                   }
-                  index.replace(groupCache.get(uuid));
+                  Optional<InternalGroup> internalGroup = groupCache.get(uuid);
+                  if (internalGroup.isPresent()) {
+                    index.replace(internalGroup.get());
+                  } else {
+                    index.delete(uuid);
+                  }
                   verboseWriter.println("Reindexed " + desc);
                   done.incrementAndGet();
                 } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java
index 3d4c92f..078433a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index.group;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.index.FieldDef.exact;
 import static com.google.gerrit.index.FieldDef.fullText;
 import static com.google.gerrit.index.FieldDef.integer;
@@ -22,40 +23,53 @@
 
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.SchemaUtil;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import java.sql.Timestamp;
 
 /** Secondary index schemas for groups. */
 public class GroupField {
   /** Legacy group ID. */
-  public static final FieldDef<AccountGroup, Integer> ID =
+  public static final FieldDef<InternalGroup, Integer> ID =
       integer("id").build(g -> g.getId().get());
 
   /** Group UUID. */
-  public static final FieldDef<AccountGroup, String> UUID =
+  public static final FieldDef<InternalGroup, String> UUID =
       exact("uuid").stored().build(g -> g.getGroupUUID().get());
 
   /** Group owner UUID. */
-  public static final FieldDef<AccountGroup, String> OWNER_UUID =
+  public static final FieldDef<InternalGroup, String> OWNER_UUID =
       exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
 
   /** Timestamp indicating when this group was created. */
-  public static final FieldDef<AccountGroup, Timestamp> CREATED_ON =
-      timestamp("created_on").build(AccountGroup::getCreatedOn);
+  public static final FieldDef<InternalGroup, Timestamp> CREATED_ON =
+      timestamp("created_on").build(InternalGroup::getCreatedOn);
 
   /** Group name. */
-  public static final FieldDef<AccountGroup, String> NAME =
-      exact("name").build(AccountGroup::getName);
+  public static final FieldDef<InternalGroup, String> NAME =
+      exact("name").build(InternalGroup::getName);
 
   /** Prefix match on group name parts. */
-  public static final FieldDef<AccountGroup, Iterable<String>> NAME_PART =
+  public static final FieldDef<InternalGroup, Iterable<String>> NAME_PART =
       prefix("name_part").buildRepeatable(g -> SchemaUtil.getNameParts(g.getName()));
 
   /** Group description. */
-  public static final FieldDef<AccountGroup, String> DESCRIPTION =
-      fullText("description").build(AccountGroup::getDescription);
+  public static final FieldDef<InternalGroup, String> DESCRIPTION =
+      fullText("description").build(InternalGroup::getDescription);
 
   /** Whether the group is visible to all users. */
-  public static final FieldDef<AccountGroup, String> IS_VISIBLE_TO_ALL =
+  public static final FieldDef<InternalGroup, String> IS_VISIBLE_TO_ALL =
       exact("is_visible_to_all").build(g -> g.isVisibleToAll() ? "1" : "0");
+
+  public static final FieldDef<InternalGroup, Iterable<Integer>> MEMBER =
+      integer("member")
+          .buildRepeatable(
+              g -> g.getMembers().stream().map(Account.Id::get).collect(toImmutableList()));
+
+  public static final FieldDef<InternalGroup, Iterable<String>> SUBGROUP =
+      exact("subgroup")
+          .buildRepeatable(
+              g ->
+                  g.getSubgroups().stream().map(AccountGroup.UUID::get).collect(toImmutableList()));
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java
index 1e56837..6a430f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -18,14 +18,15 @@
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.query.group.GroupPredicates;
 
-public interface GroupIndex extends Index<AccountGroup.UUID, AccountGroup> {
+public interface GroupIndex extends Index<AccountGroup.UUID, InternalGroup> {
   public interface Factory
-      extends IndexDefinition.IndexFactory<AccountGroup.UUID, AccountGroup, GroupIndex> {}
+      extends IndexDefinition.IndexFactory<AccountGroup.UUID, InternalGroup, GroupIndex> {}
 
   @Override
-  default Predicate<AccountGroup> keyPredicate(AccountGroup.UUID uuid) {
+  default Predicate<InternalGroup> keyPredicate(AccountGroup.UUID uuid) {
     return GroupPredicates.uuid(uuid);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
index 5ce65a8..531c446 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -17,11 +17,12 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GroupIndexCollection
-    extends IndexCollection<AccountGroup.UUID, AccountGroup, GroupIndex> {
+    extends IndexCollection<AccountGroup.UUID, InternalGroup, GroupIndex> {
   @VisibleForTesting
   public GroupIndexCollection() {}
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
index 61c3445..d117dfd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
@@ -17,10 +17,11 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.index.IndexDefinition;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 
 public class GroupIndexDefinition
-    extends IndexDefinition<AccountGroup.UUID, AccountGroup, GroupIndex> {
+    extends IndexDefinition<AccountGroup.UUID, InternalGroup, GroupIndex> {
 
   @Inject
   GroupIndexDefinition(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java
index 9ef4ba1..c658173 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java
@@ -20,12 +20,12 @@
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-public class GroupIndexRewriter implements IndexRewriter<AccountGroup> {
+public class GroupIndexRewriter implements IndexRewriter<InternalGroup> {
   private final GroupIndexCollection indexes;
 
   @Inject
@@ -34,7 +34,7 @@
   }
 
   @Override
-  public Predicate<AccountGroup> rewrite(Predicate<AccountGroup> in, QueryOptions opts)
+  public Predicate<InternalGroup> rewrite(Predicate<InternalGroup> in, QueryOptions opts)
       throws QueryParseException {
     GroupIndex index = indexes.getSearchIndex();
     checkNotNull(index, "no active search index configured for groups");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index 8c2eec9..69b29bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -21,11 +21,13 @@
 import com.google.gerrit.index.Index;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Optional;
 
 public class GroupIndexerImpl implements GroupIndexer {
   public interface Factory {
@@ -63,8 +65,13 @@
 
   @Override
   public void index(AccountGroup.UUID uuid) throws IOException {
-    for (Index<?, AccountGroup> i : getWriteIndexes()) {
-      i.replace(groupCache.get(uuid));
+    for (Index<AccountGroup.UUID, InternalGroup> i : getWriteIndexes()) {
+      Optional<InternalGroup> internalGroup = groupCache.get(uuid);
+      if (internalGroup.isPresent()) {
+        i.replace(internalGroup.get());
+      } else {
+        i.delete(uuid);
+      }
     }
     fireGroupIndexedEvent(uuid.get());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index ecd4168..b280b25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -18,11 +18,11 @@
 
 import com.google.gerrit.index.Schema;
 import com.google.gerrit.index.SchemaDefinitions;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 
-public class GroupSchemaDefinitions extends SchemaDefinitions<AccountGroup> {
+public class GroupSchemaDefinitions extends SchemaDefinitions<InternalGroup> {
   @Deprecated
-  static final Schema<AccountGroup> V2 =
+  static final Schema<InternalGroup> V2 =
       schema(
           GroupField.DESCRIPTION,
           GroupField.ID,
@@ -32,11 +32,13 @@
           GroupField.OWNER_UUID,
           GroupField.UUID);
 
-  static final Schema<AccountGroup> V3 = schema(V2, GroupField.CREATED_ON);
+  @Deprecated static final Schema<InternalGroup> V3 = schema(V2, GroupField.CREATED_ON);
+
+  static final Schema<InternalGroup> V4 = schema(V3, GroupField.MEMBER, GroupField.SUBGROUP);
 
   public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
 
   private GroupSchemaDefinitions() {
-    super("groups", AccountGroup.class);
+    super("groups", InternalGroup.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
index 5f31dd7d..255df32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -21,12 +21,15 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 
-public class IndexedGroupQuery extends IndexedQuery<AccountGroup.UUID, AccountGroup>
-    implements DataSource<AccountGroup> {
+public class IndexedGroupQuery extends IndexedQuery<AccountGroup.UUID, InternalGroup>
+    implements DataSource<InternalGroup> {
 
   public IndexedGroupQuery(
-      Index<AccountGroup.UUID, AccountGroup> index, Predicate<AccountGroup> pred, QueryOptions opts)
+      Index<AccountGroup.UUID, InternalGroup> index,
+      Predicate<InternalGroup> pred,
+      QueryOptions opts)
       throws QueryParseException {
     super(index, pred, opts.convertForBackend());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
index 63138cb..ffa59c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -16,14 +16,14 @@
 
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.index.query.IsVisibleToPredicate;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.IndexUtils;
 import com.google.gerrit.server.query.account.AccountQueryBuilder;
 import com.google.gwtorm.server.OrmException;
 
-public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<AccountGroup> {
+public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<InternalGroup> {
   protected final GroupControl.GenericFactory groupControlFactory;
   protected final CurrentUser user;
 
@@ -35,7 +35,7 @@
   }
 
   @Override
-  public boolean match(AccountGroup group) throws OrmException {
+  public boolean match(InternalGroup group) throws OrmException {
     try {
       return groupControlFactory.controlFor(user, group.getGroupUUID()).isVisible();
     } catch (NoSuchGroupException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
index c6cf3be..983d3b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -17,44 +17,54 @@
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupField;
 import java.util.Locale;
 
 public class GroupPredicates {
-  public static Predicate<AccountGroup> uuid(AccountGroup.UUID uuid) {
+  public static Predicate<InternalGroup> uuid(AccountGroup.UUID uuid) {
     return new GroupPredicate(GroupField.UUID, GroupQueryBuilder.FIELD_UUID, uuid.get());
   }
 
-  public static Predicate<AccountGroup> description(String description) {
+  public static Predicate<InternalGroup> description(String description) {
     return new GroupPredicate(
         GroupField.DESCRIPTION, GroupQueryBuilder.FIELD_DESCRIPTION, description);
   }
 
-  public static Predicate<AccountGroup> inname(String name) {
+  public static Predicate<InternalGroup> inname(String name) {
     return new GroupPredicate(
         GroupField.NAME_PART, GroupQueryBuilder.FIELD_INNAME, name.toLowerCase(Locale.US));
   }
 
-  public static Predicate<AccountGroup> name(String name) {
+  public static Predicate<InternalGroup> name(String name) {
     return new GroupPredicate(GroupField.NAME, GroupQueryBuilder.FIELD_NAME, name);
   }
 
-  public static Predicate<AccountGroup> owner(AccountGroup.UUID ownerUuid) {
+  public static Predicate<InternalGroup> owner(AccountGroup.UUID ownerUuid) {
     return new GroupPredicate(
         GroupField.OWNER_UUID, GroupQueryBuilder.FIELD_OWNER, ownerUuid.get());
   }
 
-  public static Predicate<AccountGroup> isVisibleToAll() {
+  public static Predicate<InternalGroup> isVisibleToAll() {
     return new GroupPredicate(GroupField.IS_VISIBLE_TO_ALL, "1");
   }
 
-  static class GroupPredicate extends IndexPredicate<AccountGroup> {
-    GroupPredicate(FieldDef<AccountGroup, ?> def, String value) {
+  public static Predicate<InternalGroup> member(Account.Id memberId) {
+    return new GroupPredicate(GroupField.MEMBER, memberId.toString());
+  }
+
+  public static Predicate<InternalGroup> subgroup(AccountGroup.UUID subgroupUuid) {
+    return new GroupPredicate(GroupField.SUBGROUP, subgroupUuid.get());
+  }
+
+  static class GroupPredicate extends IndexPredicate<InternalGroup> {
+    GroupPredicate(FieldDef<InternalGroup, ?> def, String value) {
       super(def, value);
     }
 
-    GroupPredicate(FieldDef<AccountGroup, ?> def, String name, String value) {
+    GroupPredicate(FieldDef<InternalGroup, ?> def, String name, String value) {
       super(def, name, value);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 1cba96c..6bd6e24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -14,23 +14,39 @@
 
 package com.google.gerrit.server.query.group;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 /** Parses a query string meant to be applied to group objects. */
-public class GroupQueryBuilder extends QueryBuilder<AccountGroup> {
+public class GroupQueryBuilder extends QueryBuilder<InternalGroup> {
   public static final String FIELD_UUID = "uuid";
   public static final String FIELD_DESCRIPTION = "description";
   public static final String FIELD_INNAME = "inname";
@@ -38,17 +54,28 @@
   public static final String FIELD_OWNER = "owner";
   public static final String FIELD_LIMIT = "limit";
 
-  private static final QueryBuilder.Definition<AccountGroup, GroupQueryBuilder> mydef =
+  private static final QueryBuilder.Definition<InternalGroup, GroupQueryBuilder> mydef =
       new QueryBuilder.Definition<>(GroupQueryBuilder.class);
 
   public static class Arguments {
+    final Provider<ReviewDb> db;
+    final GroupIndex groupIndex;
     final GroupCache groupCache;
     final GroupBackend groupBackend;
+    final AccountResolver accountResolver;
 
     @Inject
-    Arguments(GroupCache groupCache, GroupBackend groupBackend) {
+    Arguments(
+        Provider<ReviewDb> db,
+        GroupIndexCollection groupIndexCollection,
+        GroupCache groupCache,
+        GroupBackend groupBackend,
+        AccountResolver accountResolver) {
+      this.db = db;
+      this.groupIndex = groupIndexCollection.getSearchIndex();
       this.groupCache = groupCache;
       this.groupBackend = groupBackend;
+      this.accountResolver = accountResolver;
     }
   }
 
@@ -61,12 +88,12 @@
   }
 
   @Operator
-  public Predicate<AccountGroup> uuid(String uuid) {
+  public Predicate<InternalGroup> uuid(String uuid) {
     return GroupPredicates.uuid(new AccountGroup.UUID(uuid));
   }
 
   @Operator
-  public Predicate<AccountGroup> description(String description) throws QueryParseException {
+  public Predicate<InternalGroup> description(String description) throws QueryParseException {
     if (Strings.isNullOrEmpty(description)) {
       throw error("description operator requires a value");
     }
@@ -75,7 +102,7 @@
   }
 
   @Operator
-  public Predicate<AccountGroup> inname(String namePart) {
+  public Predicate<InternalGroup> inname(String namePart) {
     if (namePart.isEmpty()) {
       return name(namePart);
     }
@@ -83,25 +110,18 @@
   }
 
   @Operator
-  public Predicate<AccountGroup> name(String name) {
+  public Predicate<InternalGroup> name(String name) {
     return GroupPredicates.name(name);
   }
 
   @Operator
-  public Predicate<AccountGroup> owner(String owner) throws QueryParseException {
-    AccountGroup group = args.groupCache.get(new AccountGroup.UUID(owner));
-    if (group != null) {
-      return GroupPredicates.owner(group.getGroupUUID());
-    }
-    GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, owner);
-    if (g == null) {
-      throw error("Group " + owner + " not found");
-    }
-    return GroupPredicates.owner(g.getUUID());
+  public Predicate<InternalGroup> owner(String owner) throws QueryParseException {
+    AccountGroup.UUID groupUuid = parseGroup(owner);
+    return GroupPredicates.owner(groupUuid);
   }
 
   @Operator
-  public Predicate<AccountGroup> is(String value) throws QueryParseException {
+  public Predicate<InternalGroup> is(String value) throws QueryParseException {
     if ("visibletoall".equalsIgnoreCase(value)) {
       return GroupPredicates.isVisibleToAll();
     }
@@ -109,9 +129,9 @@
   }
 
   @Override
-  protected Predicate<AccountGroup> defaultField(String query) throws QueryParseException {
+  protected Predicate<InternalGroup> defaultField(String query) throws QueryParseException {
     // Adapt the capacity of this list when adding more default predicates.
-    List<Predicate<AccountGroup>> preds = Lists.newArrayListWithCapacity(5);
+    List<Predicate<InternalGroup>> preds = Lists.newArrayListWithCapacity(5);
     preds.add(uuid(query));
     preds.add(name(query));
     preds.add(inname(query));
@@ -127,11 +147,65 @@
   }
 
   @Operator
-  public Predicate<AccountGroup> limit(String query) throws QueryParseException {
+  public Predicate<InternalGroup> member(String query)
+      throws QueryParseException, OrmException, ConfigInvalidException, IOException {
+    if (isFieldAbsentFromIndex(GroupField.MEMBER)) {
+      throw getExceptionForUnsupportedOperator("member");
+    }
+
+    Set<Account.Id> accounts = parseAccount(query);
+    List<Predicate<InternalGroup>> predicates =
+        accounts.stream().map(GroupPredicates::member).collect(toImmutableList());
+    return Predicate.or(predicates);
+  }
+
+  @Operator
+  public Predicate<InternalGroup> subgroup(String query) throws QueryParseException {
+    if (isFieldAbsentFromIndex(GroupField.SUBGROUP)) {
+      throw getExceptionForUnsupportedOperator("subgroup");
+    }
+
+    AccountGroup.UUID groupUuid = parseGroup(query);
+    return GroupPredicates.subgroup(groupUuid);
+  }
+
+  @Operator
+  public Predicate<InternalGroup> limit(String query) throws QueryParseException {
     Integer limit = Ints.tryParse(query);
     if (limit == null) {
       throw error("Invalid limit: " + query);
     }
     return new LimitPredicate<>(FIELD_LIMIT, limit);
   }
+
+  private boolean isFieldAbsentFromIndex(FieldDef<InternalGroup, ?> field) {
+    return !args.groupIndex.getSchema().hasField(field);
+  }
+
+  private static QueryParseException getExceptionForUnsupportedOperator(String operatorName) {
+    return new QueryParseException(
+        String.format("'%s' operator is not supported by group index version", operatorName));
+  }
+
+  private Set<Account.Id> parseAccount(String nameOrEmail)
+      throws QueryParseException, OrmException, IOException, ConfigInvalidException {
+    Set<Account.Id> foundAccounts = args.accountResolver.findAll(args.db.get(), nameOrEmail);
+    if (foundAccounts.isEmpty()) {
+      throw error("User " + nameOrEmail + " not found");
+    }
+    return foundAccounts;
+  }
+
+  private AccountGroup.UUID parseGroup(String groupNameOrUuid) throws QueryParseException {
+    Optional<InternalGroup> group = args.groupCache.get(new AccountGroup.UUID(groupNameOrUuid));
+    if (group.isPresent()) {
+      return group.get().getGroupUUID();
+    }
+    GroupReference groupReference =
+        GroupBackends.findBestSuggestion(args.groupBackend, groupNameOrUuid);
+    if (groupReference == null) {
+      throw error("Group " + groupNameOrUuid + " not found");
+    }
+    return groupReference.getUUID();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index a1f4a31..8554ecf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -23,10 +23,10 @@
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryProcessor;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountLimits;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.index.group.GroupIndexRewriter;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
@@ -39,7 +39,7 @@
  * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
  * holding on to a single instance.
  */
-public class GroupQueryProcessor extends QueryProcessor<AccountGroup> {
+public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
   private final Provider<CurrentUser> userProvider;
   private final GroupControl.GenericFactory groupControlFactory;
 
@@ -72,7 +72,7 @@
   }
 
   @Override
-  protected Predicate<AccountGroup> enforceVisibility(Predicate<AccountGroup> pred) {
+  protected Predicate<InternalGroup> enforceVisibility(Predicate<InternalGroup> pred) {
     return new AndSource<>(
         pred, new GroupIsVisibleToPredicate(groupControlFactory, userProvider.get()), start);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 5ba83d6..8c1ccd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -25,6 +26,7 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.group.GroupsUpdate;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -100,16 +102,16 @@
     admin = newGroup(db, "Administrators");
     admin.setDescription("Gerrit Site Administrators");
     GroupsUpdate.addNewGroup(db, admin);
-    index(admin);
+    index(InternalGroup.create(admin, ImmutableSet.of(), ImmutableSet.of()));
 
     batch = newGroup(db, "Non-Interactive Users");
     batch.setDescription("Users who perform batch actions on Gerrit");
     batch.setOwnerGroupUUID(admin.getGroupUUID());
     GroupsUpdate.addNewGroup(db, batch);
-    index(batch);
+    index(InternalGroup.create(batch, ImmutableSet.of(), ImmutableSet.of()));
   }
 
-  private void index(AccountGroup group) throws IOException {
+  private void index(InternalGroup group) throws IOException {
     for (GroupIndex groupIndex : indexCollection.getWriteIndexes()) {
       groupIndex.replace(group);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 6b94e02..1e506ed 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -16,14 +16,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.accounts.AccountInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.groups.Groups.QueryRequest;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.Schema;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -39,7 +43,10 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.group.GroupsUpdate;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.ServerInitiated;
+import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
@@ -99,6 +106,8 @@
 
   @Inject @ServerInitiated protected Provider<GroupsUpdate> groupsUpdateProvider;
 
+  @Inject protected GroupIndexCollection indexes;
+
   protected LifecycleManager lifecycle;
   protected Injector injector;
   protected ReviewDb db;
@@ -121,7 +130,8 @@
     db = schemaFactory.open();
     schemaCreator.create(db);
 
-    Account.Id userId = createAccount("user", "User", "user@example.com", true);
+    Account.Id userId =
+        createAccountOutsideRequestContext("user", "User", "user@example.com", true);
     user = userFactory.create(userId);
     requestContext.setContext(newRequestContext(userId));
     currentUserInfo = gApi.accounts().id(userId.get()).get();
@@ -162,7 +172,9 @@
     if (lifecycle != null) {
       lifecycle.stop();
     }
-    requestContext.setContext(null);
+    if (requestContext != null) {
+      requestContext.setContext(null);
+    }
     if (db != null) {
       db.close();
     }
@@ -247,6 +259,58 @@
   }
 
   @Test
+  public void byMember() throws Exception {
+    if (getSchemaVersion() < 4) {
+      assertMissingField(GroupField.MEMBER);
+      assertFailingQuery(
+          "member:someName", "'member' operator is not supported by group index version");
+      return;
+    }
+
+    AccountInfo user1 = createAccount("user1", "User1", "user1@example.com");
+    AccountInfo user2 = createAccount("user2", "User2", "user2@example.com");
+
+    GroupInfo group1 = createGroup(name("group1"), user1);
+    GroupInfo group2 = createGroup(name("group2"), user2);
+    GroupInfo group3 = createGroup(name("group3"), user1);
+
+    assertQuery("member:" + user1.name, group1, group3);
+    assertQuery("member:" + user1.email, group1, group3);
+
+    gApi.groups().id(group3.id).removeMembers(user1.username);
+    gApi.groups().id(group2.id).addMembers(user1.username);
+
+    assertQuery("member:" + user1.name, group1, group2);
+  }
+
+  @Test
+  public void bySubgroups() throws Exception {
+    if (getSchemaVersion() < 4) {
+      assertMissingField(GroupField.SUBGROUP);
+      assertFailingQuery(
+          "subgroup:someGroupName", "'subgroup' operator is not supported by group index version");
+      return;
+    }
+
+    GroupInfo superParentGroup = createGroup(name("superParentGroup"));
+    GroupInfo parentGroup1 = createGroup(name("parentGroup1"));
+    GroupInfo parentGroup2 = createGroup(name("parentGroup2"));
+    GroupInfo subGroup = createGroup(name("subGroup"));
+
+    gApi.groups().id(superParentGroup.id).addGroups(parentGroup1.id, parentGroup2.id);
+    gApi.groups().id(parentGroup1.id).addGroups(subGroup.id);
+    gApi.groups().id(parentGroup2.id).addGroups(subGroup.id);
+
+    assertQuery("subgroup:" + subGroup.id, parentGroup1, parentGroup2);
+    assertQuery("subgroup:" + parentGroup1.id, superParentGroup);
+
+    gApi.groups().id(superParentGroup.id).addGroups(subGroup.id);
+    gApi.groups().id(parentGroup1.id).removeGroups(subGroup.id);
+
+    assertQuery("subgroup:" + subGroup.id, superParentGroup, parentGroup2);
+  }
+
+  @Test
   public void byDefaultField() throws Exception {
     GroupInfo group1 = createGroup(name("foo-group"));
     GroupInfo group2 = createGroup(name("group2"));
@@ -313,8 +377,8 @@
     assertQuery("description:" + newDescription, group1);
   }
 
-  private Account.Id createAccount(String username, String fullName, String email, boolean active)
-      throws Exception {
+  private Account.Id createAccountOutsideRequestContext(
+      String username, String fullName, String email, boolean active) throws Exception {
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
       Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
       if (email != null) {
@@ -333,6 +397,16 @@
     }
   }
 
+  protected AccountInfo createAccount(String username, String fullName, String email)
+      throws Exception {
+    String uniqueName = name(username);
+    AccountInput accountInput = new AccountInput();
+    accountInput.username = uniqueName;
+    accountInput.name = fullName;
+    accountInput.email = email;
+    return gApi.accounts().create(accountInput).get();
+  }
+
   protected GroupInfo createGroup(String name, AccountInfo... members) throws Exception {
     return createGroupWithDescription(name, null, members);
   }
@@ -452,4 +526,27 @@
 
     return name + "_" + getSanitizedMethodName();
   }
+
+  protected void assertMissingField(FieldDef<InternalGroup, ?> field) {
+    assertThat(getSchema().hasField(field))
+        .named("schema %s has field %s", getSchemaVersion(), field.getName())
+        .isFalse();
+  }
+
+  protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
+    try {
+      assertQuery(query);
+      fail("expected BadRequestException for query '" + query + "'");
+    } catch (BadRequestException e) {
+      assertThat(e.getMessage()).isEqualTo(expectedMessage);
+    }
+  }
+
+  protected int getSchemaVersion() {
+    return getSchema().getVersion();
+  }
+
+  protected Schema<InternalGroup> getSchema() {
+    return indexes.getSearchIndex().getSchema();
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index a742c35..6b5d632 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -25,8 +25,8 @@
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.group.AddIncludedGroups;
 import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.group.AddSubgroups;
 import com.google.gerrit.server.group.CreateGroup;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.GroupsCollection;
@@ -101,7 +101,7 @@
 
   @Inject private AddMembers addMembers;
 
-  @Inject private AddIncludedGroups addIncludedGroups;
+  @Inject private AddSubgroups addSubgroups;
 
   @Override
   protected void run() throws Failure, OrmException, IOException, ConfigInvalidException {
@@ -113,7 +113,7 @@
       }
 
       if (!initialGroups.isEmpty()) {
-        addIncludedGroups(rsrc);
+        addSubgroups(rsrc);
       }
     } catch (RestApiException e) {
       throw die(e);
@@ -142,10 +142,10 @@
     addMembers.apply(rsrc, input);
   }
 
-  private void addIncludedGroups(GroupResource rsrc) throws RestApiException, OrmException {
-    AddIncludedGroups.Input input =
-        AddIncludedGroups.Input.fromGroups(
+  private void addSubgroups(GroupResource rsrc) throws RestApiException, OrmException, IOException {
+    AddSubgroups.Input input =
+        AddSubgroups.Input.fromGroups(
             initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
-    addIncludedGroups.apply(rsrc, input);
+    addSubgroups.apply(rsrc, input);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index 2d42f01..ac3784a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -22,12 +22,14 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.Options;
 import com.google.inject.Inject;
+import java.util.Optional;
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(
@@ -60,15 +62,15 @@
     for (GroupInfo info : listGroups.get()) {
       formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));
       if (verboseOutput) {
-        AccountGroup o =
+        Optional<InternalGroup> group =
             info.ownerId != null
                 ? groupCache.get(new AccountGroup.UUID(Url.decode(info.ownerId)))
-                : null;
+                : Optional.empty();
 
         formatter.addColumn(Url.decode(info.id));
         formatter.addColumn(Strings.nullToEmpty(info.description));
-        formatter.addColumn(o != null ? o.getName() : "n/a");
-        formatter.addColumn(o != null ? o.getGroupUUID().get() : "");
+        formatter.addColumn(group.map(InternalGroup::getName).orElse("n/a"));
+        formatter.addColumn(group.map(g -> g.getGroupUUID().get()).orElse(""));
         formatter.addColumn(
             Boolean.toString(MoreObjects.firstNonNull(info.options.visibleToAll, Boolean.FALSE)));
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index ca7b18b..1c903c7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupDetailFactory.Factory;
+import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.ListMembers;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -61,9 +61,9 @@
     @Inject
     protected ListMembersCommandImpl(
         GroupCache groupCache,
-        Factory groupDetailFactory,
+        GroupControl.Factory groupControlFactory,
         AccountLoader.Factory accountLoaderFactory) {
-      super(groupCache, groupDetailFactory, accountLoaderFactory);
+      super(groupCache, groupControlFactory, accountLoaderFactory);
       this.groupCache = groupCache;
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index ae47175..9062b52 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -18,6 +18,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.Streams;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -25,12 +26,13 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.group.AddIncludedGroups;
 import com.google.gerrit.server.group.AddMembers;
-import com.google.gerrit.server.group.DeleteIncludedGroups;
+import com.google.gerrit.server.group.AddSubgroups;
 import com.google.gerrit.server.group.DeleteMembers;
+import com.google.gerrit.server.group.DeleteSubgroups;
 import com.google.gerrit.server.group.GroupResource;
 import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -92,9 +94,9 @@
 
   @Inject private DeleteMembers deleteMembers;
 
-  @Inject private AddIncludedGroups addIncludedGroups;
+  @Inject private AddSubgroups addSubgroups;
 
-  @Inject private DeleteIncludedGroups deleteIncludedGroups;
+  @Inject private DeleteSubgroups deleteSubgroups;
 
   @Inject private GroupsCollection groupsCollection;
 
@@ -113,7 +115,7 @@
           reportMembersAction("removed from", resource, accountsToRemove);
         }
         if (!groupsToRemove.isEmpty()) {
-          deleteIncludedGroups.apply(resource, fromGroups(groupsToRemove));
+          deleteSubgroups.apply(resource, fromGroups(groupsToRemove));
           reportGroupsAction("excluded from", resource, groupsToRemove);
         }
         if (!accountsToAdd.isEmpty()) {
@@ -121,7 +123,7 @@
           reportMembersAction("added to", resource, accountsToAdd);
         }
         if (!groupsToInclude.isEmpty()) {
-          addIncludedGroups.apply(resource, fromGroups(groupsToInclude));
+          addSubgroups.apply(resource, fromGroups(groupsToInclude));
           reportGroupsAction("included to", resource, groupsToInclude);
         }
       }
@@ -149,14 +151,17 @@
       String action, GroupResource group, List<AccountGroup.UUID> groupUuidList)
       throws UnsupportedEncodingException, IOException {
     String names =
-        groupUuidList.stream().map(uuid -> groupCache.get(uuid).getName()).collect(joining(", "));
+        groupUuidList
+            .stream()
+            .map(uuid -> groupCache.get(uuid).map(InternalGroup::getName))
+            .flatMap(Streams::stream)
+            .collect(joining(", "));
     out.write(
         String.format("Groups %s group %s: %s\n", action, group.getName(), names).getBytes(ENC));
   }
 
-  private AddIncludedGroups.Input fromGroups(List<AccountGroup.UUID> accounts) {
-    return AddIncludedGroups.Input.fromGroups(
-        accounts.stream().map(Object::toString).collect(toList()));
+  private AddSubgroups.Input fromGroups(List<AccountGroup.UUID> accounts) {
+    return AddSubgroups.Input.fromGroups(accounts.stream().map(Object::toString).collect(toList()));
   }
 
   private AddMembers.Input fromMembers(List<Account.Id> accounts) {