You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ju...@apache.org on 2019/10/09 07:40:03 UTC

[fineract-cn-group] 01/50: Initial Commit

This is an automated email from the ASF dual-hosted git repository.

juhan pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract-cn-group.git

commit fe3968e6f7198ce8670b93a1fe9e8d528e7326ba
Author: Markus Geiss <mg...@mifos.org>
AuthorDate: Thu Mar 16 14:48:55 2017 +0100

    Initial Commit
---
 .gitignore                                         |  16 +
 HEADER                                             |  13 +
 LICENSE                                            | 201 +++++++++++++
 README.md                                          |  25 ++
 api/build.gradle                                   |  33 ++
 api/settings.gradle                                |   1 +
 .../java/io/mifos/group/api/v1/EventConstants.java |  45 +++
 .../group/api/v1/client/GroupAlreadyExists.java    |  19 ++
 .../io/mifos/group/api/v1/client/GroupClient.java  | 174 +++++++++++
 .../v1/client/GroupDefinitionAlreadyExists.java    |  19 ++
 .../api/v1/client/GroupDefinitionNotFound.java     |  19 ++
 .../api/v1/client/GroupDefinitionValidation.java   |  19 ++
 .../api/v1/client/GroupNotFoundException.java      |  19 ++
 .../api/v1/client/GroupValidationException.java    |  19 ++
 .../java/io/mifos/group/api/v1/domain/Address.java |  83 ++++++
 .../api/v1/domain/AssignedEmployeeHolder.java      |  33 ++
 .../io/mifos/group/api/v1/domain/Attendee.java     |  54 ++++
 .../java/io/mifos/group/api/v1/domain/Cycle.java   |  67 +++++
 .../java/io/mifos/group/api/v1/domain/Group.java   | 202 +++++++++++++
 .../io/mifos/group/api/v1/domain/GroupCommand.java |  66 ++++
 .../mifos/group/api/v1/domain/GroupDefinition.java | 116 +++++++
 .../io/mifos/group/api/v1/domain/GroupPage.java    |  53 ++++
 .../java/io/mifos/group/api/v1/domain/Meeting.java | 127 ++++++++
 .../mifos/group/api/v1/domain/SignOffMeeting.java  |  64 ++++
 build.gradle                                       |  29 ++
 component-test/build.gradle                        |  38 +++
 component-test/settings.gradle                     |   1 +
 .../src/main/java/io/mifos/group/TestGroup.java    | 275 +++++++++++++++++
 .../java/io/mifos/group/TestGroupDefinition.java   | 139 +++++++++
 .../listener/GroupDefinitionEventListener.java     |  47 +++
 .../mifos/group/listener/GroupEventListener.java   |  67 +++++
 .../group/listener/MigrationEventListener.java     |  47 +++
 .../mifos/group/util/GroupDefinitionGenerator.java |  41 +++
 .../java/io/mifos/group/util/GroupGenerator.java   |  59 ++++
 component-test/src/main/resources/logback.xml      |  28 ++
 gradle/wrapper/gradle-wrapper.jar                  | Bin 0 -> 54212 bytes
 gradle/wrapper/gradle-wrapper.properties           |   6 +
 gradlew                                            | 172 +++++++++++
 gradlew.bat                                        |  84 ++++++
 service/build.gradle                               |  63 ++++
 service/settings.gradle                            |   1 +
 .../io/mifos/group/service/GroupApplication.java   |  29 ++
 .../io/mifos/group/service/GroupConfiguration.java |  64 ++++
 .../io/mifos/group/service/ServiceConstants.java   |  20 ++
 .../internal/command/ActivateGroupCommand.java     |  38 +++
 .../internal/command/CloseGroupCommand.java        |  38 +++
 .../internal/command/CreateGroupCommand.java       |  32 ++
 .../command/CreateGroupDefinitionCommand.java      |  32 ++
 .../internal/command/InitializeServiceCommand.java |  23 ++
 .../internal/command/ReopenGroupCommand.java       |  38 +++
 .../internal/command/SignOffMeetingCommand.java    |  38 +++
 .../command/UpdateAssignedEmployeeCommand.java     |  36 +++
 .../internal/command/UpdateLeadersCommand.java     |  38 +++
 .../internal/command/UpdateMembersCommand.java     |  38 +++
 .../internal/command/handler/GroupAggregate.java   | 332 +++++++++++++++++++++
 .../command/handler/MigrationAggregate.java        |  58 ++++
 .../service/internal/mapper/AddressMapper.java     |  48 +++
 .../service/internal/mapper/AttendeeMapper.java    |  33 ++
 .../internal/mapper/GroupCommandMapper.java        |  49 +++
 .../internal/mapper/GroupDefinitionMapper.java     |  51 ++++
 .../group/service/internal/mapper/GroupMapper.java |  48 +++
 .../service/internal/mapper/MeetingMapper.java     |  43 +++
 .../service/internal/repository/AddressEntity.java | 105 +++++++
 .../internal/repository/AddressRepository.java     |  23 ++
 .../internal/repository/AttendeeEntity.java        |  79 +++++
 .../internal/repository/AttendeeRepository.java    |  27 ++
 .../internal/repository/GroupCommandEntity.java    | 104 +++++++
 .../repository/GroupCommandRepository.java         |  27 ++
 .../internal/repository/GroupDefinitionEntity.java | 161 ++++++++++
 .../repository/GroupDefinitionRepository.java      |  27 ++
 .../service/internal/repository/GroupEntity.java   | 207 +++++++++++++
 .../internal/repository/GroupRepository.java       |  31 ++
 .../service/internal/repository/MeetingEntity.java | 138 +++++++++
 .../internal/repository/MeetingRepository.java     |  39 +++
 .../internal/service/GroupDefinitionService.java   |  55 ++++
 .../service/internal/service/GroupService.java     | 134 +++++++++
 .../rest/GroupDefinitionRestController.java        | 104 +++++++
 .../group/service/rest/GroupRestController.java    | 269 +++++++++++++++++
 .../service/rest/MigrationRestController.java      |  60 ++++
 service/src/main/resources/application.yml         |  67 +++++
 service/src/main/resources/bootstrap.yml           |  19 ++
 .../db/migrations/mariadb/V1__initial_setup.sql    | 100 +++++++
 settings.gradle                                    |   5 +
 shared.gradle                                      |  64 ++++
 84 files changed, 5555 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..95274b9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+.gradle
+.idea
+**/build/
+**/target/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+*.iml
+
+*.log
+
+*.toDelete
\ No newline at end of file
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..76f9222
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,13 @@
+Copyright ${year} ${name}
+
+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.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bec5de8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# Mifos I/O Group Management
+
+[![Join the chat at https://gitter.im/mifos-initiative/mifos.io](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mifos-initiative/mifos.io?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+This project provides Group management capabilities.
+
+## Versioning
+The version numbers follow the [Semantic Versioning](http://semver.org/) scheme.
+
+In addition to MAJOR.MINOR.PATCH the following postfixes are used to indicate the development state.
+
+* BUILD-SNAPSHOT - A release currently in development. 
+* M - A _milestone_ release include specific sets of functions and are released as soon as the functionality is complete.
+* RC - A _release candidate_ is a version with potential to be a final product, considered _code complete_.
+* RELEASE - _General availability_ indicates that this release is the best available version and is recommended for all usage.
+
+The versioning layout is {MAJOR}.{MINOR}.{PATCH}-{INDICATOR}[.{PATCH}]. Only milestones and release candidates can  have patch versions. Some examples:
+
+1.2.3.BUILD-SNAPSHOT  
+1.3.5.M.1  
+1.5.7.RC.2  
+2.0.0.RELEASE
+
+## License
+See [LICENSE](LICENSE) file.
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 0000000..5dc9656
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,33 @@
+buildscript {
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'io.spring.gradle:dependency-management-plugin:0.6.0.RELEASE' }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+    compile(
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
+            [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+    )
+}
+
+publishing {
+    publications {
+        api(MavenPublication) {
+            from components.java
+            groupId project.group
+            artifactId project.name
+            version project.version
+        }
+    }
+}
diff --git a/api/settings.gradle b/api/settings.gradle
new file mode 100644
index 0000000..5cd7dd3
--- /dev/null
+++ b/api/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'api'
diff --git a/api/src/main/java/io/mifos/group/api/v1/EventConstants.java b/api/src/main/java/io/mifos/group/api/v1/EventConstants.java
new file mode 100644
index 0000000..f18fe33
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/EventConstants.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1;
+
+@SuppressWarnings("unused")
+public interface EventConstants {
+
+  String DESTINATION = "group-v1";
+  String SELECTOR_NAME = "action";
+
+  String INITIALIZE = "initialize";
+  String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'";
+
+  String POST_GROUP_DEFINITION = "post-group-definition";
+  String SELECTOR_POST_GROUP_DEFINITION = SELECTOR_NAME + " = '" + POST_GROUP_DEFINITION + "'";
+
+  String POST_GROUP = "post-group";
+  String SELECTOR_POST_GROUP = SELECTOR_NAME + " = '" + POST_GROUP + "'";
+  String PUT_GROUP = "put-group";
+  String SELECTOR_PUT_GROUP = SELECTOR_NAME + " = '" + PUT_GROUP + "'";
+  String ACTIVATE_GROUP = "activate-group";
+  String SELECTOR_ACTIVATE_GROUP = SELECTOR_NAME + " = '" + ACTIVATE_GROUP + "'";
+  String CLOSE_GROUP = "close-group";
+  String SELECTOR_CLOSE_GROUP = SELECTOR_NAME + " = '" + CLOSE_GROUP + "'";
+  String REOPEN_GROUP = "reopen-group";
+  String SELECTOR_REOPEN_GROUP = SELECTOR_NAME + " = '" + REOPEN_GROUP + "'";
+
+  String POST_MEETING = "post-meeting";
+  String SELECTOR_POST_MEETING = SELECTOR_NAME + " = '" + POST_MEETING + "'";
+  String PUT_MEETING = "put-meeting";
+  String SELECTOR_PUT_MEETING = SELECTOR_NAME + " = '" + PUT_MEETING + "'";
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupAlreadyExists.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupAlreadyExists.java
new file mode 100644
index 0000000..35fd6e2
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupAlreadyExists.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupAlreadyExists extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupClient.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupClient.java
new file mode 100644
index 0000000..f1e44b9
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupClient.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+import io.mifos.core.api.annotation.ThrowsException;
+import io.mifos.core.api.annotation.ThrowsExceptions;
+import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.group.api.v1.domain.AssignedEmployeeHolder;
+import io.mifos.group.api.v1.domain.SignOffMeeting;
+import io.mifos.group.api.v1.domain.Group;
+import io.mifos.group.api.v1.domain.GroupCommand;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import io.mifos.group.api.v1.domain.GroupPage;
+import io.mifos.group.api.v1.domain.Meeting;
+import org.springframework.cloud.netflix.feign.FeignClient;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.List;
+import java.util.Set;
+
+@SuppressWarnings("unused")
+@FeignClient(name="group-v1", path="/group/v1", configuration=CustomFeignClientsConfiguration.class)
+public interface GroupClient {
+
+  @RequestMapping(
+      value = "/definitions",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = GroupDefinitionAlreadyExists.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = GroupDefinitionValidation.class)
+  })
+  void createGroupDefinition(@RequestBody final GroupDefinition groupDefinition);
+
+  @RequestMapping(
+      value = "/definitions/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ResponseBody
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupDefinitionNotFound.class)
+  GroupDefinition findGroupDefinition(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/definitions",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ResponseBody
+  List<GroupDefinition> fetchGroupDefinitions();
+
+  @RequestMapping(
+      value = "/groups",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsExceptions({
+      @ThrowsException(status = HttpStatus.CONFLICT, exception = GroupAlreadyExists.class),
+      @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = GroupValidationException.class)
+  })
+  void createGroup(@RequestBody final Group group);
+
+  @RequestMapping(
+      value = "/groups",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  GroupPage fetchGroups(@RequestParam("employee") final String employee,
+                        @RequestParam("page") final Integer page,
+                        @RequestParam("size") final Integer size,
+                        @RequestParam("sortColumn") final String sortColumn,
+                        @RequestParam("sortDirection") final String sortDirection);
+
+  @RequestMapping(
+      value = "/groups/{identifier}",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  Group findGroup(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/leaders",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  void updateLeaders(@PathVariable("identifier") final String identifier, final Set<String> customerIdentifiers);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/members",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  void updateMembers(@PathVariable("identifier") final String identifier, final Set<String> customerIdentifiers);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/employee",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  void updateAssignedEmployee(@PathVariable("identifier") final String identifier,
+                              final AssignedEmployeeHolder assignedEmployeeHolder);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/commands",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  void processGroupCommand(@PathVariable("identifier") final String identifier, final GroupCommand groupCommand);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/commands",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  List<GroupCommand> fetchGroupCommands(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/meetings",
+      method = RequestMethod.GET,
+      produces = MediaType.ALL_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  List<Meeting> fetchMeetings(
+      @PathVariable("identifier") final String groupIdentifier,
+      @RequestParam("upcoming") final Boolean upcoming);
+
+  @RequestMapping(
+      value = "/groups/{identifier}/meetings",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  @ThrowsException(status = HttpStatus.NOT_FOUND, exception = GroupNotFoundException.class)
+  void closeMeeting(@PathVariable("identifier") final String groupIdentifier, final SignOffMeeting signOffMeeting);
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionAlreadyExists.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionAlreadyExists.java
new file mode 100644
index 0000000..261411a
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionAlreadyExists.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupDefinitionAlreadyExists extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionNotFound.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionNotFound.java
new file mode 100644
index 0000000..99e2676
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionNotFound.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupDefinitionNotFound extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionValidation.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionValidation.java
new file mode 100644
index 0000000..75be3a1
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupDefinitionValidation.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupDefinitionValidation extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupNotFoundException.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupNotFoundException.java
new file mode 100644
index 0000000..629c5e6
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupNotFoundException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupNotFoundException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/client/GroupValidationException.java b/api/src/main/java/io/mifos/group/api/v1/client/GroupValidationException.java
new file mode 100644
index 0000000..2a3bd0b
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/client/GroupValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.client;
+
+public class GroupValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/Address.java b/api/src/main/java/io/mifos/group/api/v1/domain/Address.java
new file mode 100644
index 0000000..902b255
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/Address.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+public class Address {
+
+  @NotBlank
+  private String street;
+  @NotBlank
+  private String city;
+  private String region;
+  private String postalCode;
+  @NotBlank
+  private String countryCode;
+  private String country;
+
+  public Address() {
+    super();
+  }
+
+  public String getStreet() {
+    return this.street;
+  }
+
+  public void setStreet(final String street) {
+    this.street = street;
+  }
+
+  public String getCity() {
+    return this.city;
+  }
+
+  public void setCity(final String city) {
+    this.city = city;
+  }
+
+  public String getRegion() {
+    return this.region;
+  }
+
+  public void setRegion(final String region) {
+    this.region = region;
+  }
+
+  public String getPostalCode() {
+    return this.postalCode;
+  }
+
+  public void setPostalCode(final String postalCode) {
+    this.postalCode = postalCode;
+  }
+
+  public String getCountryCode() {
+    return this.countryCode;
+  }
+
+  public void setCountryCode(final String countryCode) {
+    this.countryCode = countryCode;
+  }
+
+  public String getCountry() {
+    return this.country;
+  }
+
+  public void setCountry(final String country) {
+    this.country = country;
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/AssignedEmployeeHolder.java b/api/src/main/java/io/mifos/group/api/v1/domain/AssignedEmployeeHolder.java
new file mode 100644
index 0000000..e2ce5d8
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/AssignedEmployeeHolder.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+public class AssignedEmployeeHolder {
+
+  private String identifier;
+
+  public AssignedEmployeeHolder() {
+    super();
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/Attendee.java b/api/src/main/java/io/mifos/group/api/v1/domain/Attendee.java
new file mode 100644
index 0000000..147be78
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/Attendee.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.NotNull;
+
+public class Attendee {
+
+  @NotBlank
+  private String customerIdentifier;
+  @NotNull
+  private Status status;
+
+  public Attendee() {
+    super();
+  }
+
+  public String getCustomerIdentifier() {
+    return this.customerIdentifier;
+  }
+
+  public void setCustomerIdentifier(final String customerIdentifier) {
+    this.customerIdentifier = customerIdentifier;
+  }
+
+  public String getStatus() {
+    return this.status.name();
+  }
+
+  public void setStatus(final String status) {
+    this.status = Status.valueOf(status);
+  }
+
+  public enum Status {
+    EXPECTED,
+    ATTENDED,
+    MISSED
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/Cycle.java b/api/src/main/java/io/mifos/group/api/v1/domain/Cycle.java
new file mode 100644
index 0000000..261c651
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/Cycle.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import javax.validation.constraints.NotNull;
+
+public class Cycle {
+
+  @NotNull
+  private Integer numberOfMeetings;
+  @NotNull
+  private Frequency frequency;
+  private Adjustment adjustment;
+
+  public Cycle() {
+    super();
+  }
+
+  public Integer getNumberOfMeetings() {
+    return this.numberOfMeetings;
+  }
+
+  public void setNumberOfMeetings(final Integer numberOfMeetings) {
+    this.numberOfMeetings = numberOfMeetings;
+  }
+
+  public String getFrequency() {
+    return this.frequency.name();
+  }
+
+  public void setFrequency(final String frequency) {
+    this.frequency = Frequency.valueOf(frequency);
+  }
+
+  public String getAdjustment() {
+    return this.adjustment.name();
+  }
+
+  public void setAdjustment(final String adjustment) {
+    this.adjustment = Adjustment.valueOf(adjustment);
+  }
+
+  public enum Frequency {
+    DAILY,
+    WEEKLY,
+    FORTNIGHTLY,
+    MONTHLY
+  }
+
+  public enum Adjustment {
+    NEXT_BUSINESS_DAY,
+    SKIP
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/Group.java b/api/src/main/java/io/mifos/group/api/v1/domain/Group.java
new file mode 100644
index 0000000..c38d955
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/Group.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.Objects;
+import java.util.Set;
+
+public class Group {
+
+  @NotBlank
+  private String identifier;
+  @NotBlank
+  private String groupDefinitionIdentifier;
+  @NotBlank
+  private String name;
+  @Size(max = 15)
+  private Set<String> leaders;
+  @Size(min = 1, max = 60)
+  private Set<String> members;
+  @NotBlank
+  private String office;
+  @NotBlank
+  private String assignedEmployee;
+  @NotNull
+  private Integer weekday;
+  private Status status;
+  @Valid
+  private Address address;
+  private String createdOn;
+  private String createdBy;
+  private String lastModifiedOn;
+  private String lastModifiedBy;
+
+  public Group() {
+    super();
+  }
+
+  public String getGroupDefinitionIdentifier() {
+    return this.groupDefinitionIdentifier;
+  }
+
+  public void setGroupDefinitionIdentifier(final String groupDefinitionIdentifier) {
+    this.groupDefinitionIdentifier = groupDefinitionIdentifier;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public Set<String> getLeaders() {
+    return this.leaders;
+  }
+
+  public void setLeaders(final Set<String> leaders) {
+    this.leaders = leaders;
+  }
+
+  public Set<String> getMembers() {
+    return this.members;
+  }
+
+  public void setMembers(final Set<String> members) {
+    this.members = members;
+  }
+
+  public String getOffice() {
+    return this.office;
+  }
+
+  public void setOffice(final String office) {
+    this.office = office;
+  }
+
+  public String getAssignedEmployee() {
+    return this.assignedEmployee;
+  }
+
+  public void setAssignedEmployee(final String assignedEmployee) {
+    this.assignedEmployee = assignedEmployee;
+  }
+
+  public String getStatus() {
+    return this.status.name();
+  }
+
+  public void setStatus(final String status) {
+    this.status = Status.valueOf(status);
+  }
+
+  public Integer getWeekday() {
+    return this.weekday;
+  }
+
+  public void setWeekday(final Integer weekday) {
+    this.weekday = weekday;
+  }
+
+  public Address getAddress() {
+    return this.address;
+  }
+
+  public void setAddress(final Address address) {
+    this.address = address;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final String lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+
+  public enum Weekday {
+    MONDAY(1),
+    TUESDAY(2),
+    WEDNESDAY(3),
+    THURSDAY(4),
+    FRIDAY(5),
+    SATURDAY(6),
+    SUNDAY(7);
+
+    private Integer value;
+
+    Weekday(final Integer value) {
+      this.value = value;
+    }
+
+    public static Weekday from(final Integer dayOfWeek) {
+      for (Weekday weekday : Weekday.values()) {
+        if (Objects.equals(weekday.value, dayOfWeek)) {
+          return weekday;
+        }
+      }
+      throw new IllegalArgumentException("Unknown day of week '" + dayOfWeek + "'.");
+    }
+
+    public Integer getValue() {
+      return this.value;
+    }
+  }
+
+  public enum Status {
+    PENDING,
+    ACTIVE,
+    CLOSED
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/GroupCommand.java b/api/src/main/java/io/mifos/group/api/v1/domain/GroupCommand.java
new file mode 100644
index 0000000..1941d47
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/GroupCommand.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+public class GroupCommand {
+
+  private Action action;
+  private String note;
+  private String createdBy;
+  private String createdOn;
+
+  public GroupCommand() {
+    super();
+  }
+
+  public String getAction() {
+    return this.action.name();
+  }
+
+  public void setAction(final String action) {
+    this.action = Action.valueOf(action);
+  }
+
+  public String getNote() {
+    return this.note;
+  }
+
+  public void setNote(final String note) {
+    this.note = note;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public enum Action {
+    ACTIVATE,
+    CLOSE,
+    REOPEN
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/GroupDefinition.java b/api/src/main/java/io/mifos/group/api/v1/domain/GroupDefinition.java
new file mode 100644
index 0000000..70f7726
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/GroupDefinition.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+public class GroupDefinition {
+
+  @NotBlank
+  private String identifier;
+  private String description;
+  @NotNull
+  @Min(1L)
+  private Integer minimalSize;
+  @Min(2L)
+  private Integer maximalSize;
+  @Valid
+  private Cycle cycle;
+  private String createOn;
+  private String createdBy;
+  private String lastModifiedOn;
+  private String lastModifiedBy;
+
+  public GroupDefinition() {
+    super();
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public Integer getMinimalSize() {
+    return this.minimalSize;
+  }
+
+  public void setMinimalSize(final Integer minimalSize) {
+    this.minimalSize = minimalSize;
+  }
+
+  public Integer getMaximalSize() {
+    return this.maximalSize;
+  }
+
+  public void setMaximalSize(final Integer maximalSize) {
+    this.maximalSize = maximalSize;
+  }
+
+  public Cycle getCycle() {
+    return this.cycle;
+  }
+
+  public void setCycle(final Cycle cycle) {
+    this.cycle = cycle;
+  }
+
+  public String getCreateOn() {
+    return this.createOn;
+  }
+
+  public void setCreateOn(final String createOn) {
+    this.createOn = createOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public String getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final String lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/GroupPage.java b/api/src/main/java/io/mifos/group/api/v1/domain/GroupPage.java
new file mode 100644
index 0000000..7ddd739
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/GroupPage.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import java.util.List;
+
+public class GroupPage {
+
+  private List<Group> groups;
+  private Integer totalPages;
+  private Long totalElements;
+
+  public GroupPage() {
+    super();
+  }
+
+  public List<Group> getGroups() {
+    return this.groups;
+  }
+
+  public void setGroups(final List<Group> groups) {
+    this.groups = groups;
+  }
+
+  public Integer getTotalPages() {
+    return this.totalPages;
+  }
+
+  public void setTotalPages(final Integer totalPages) {
+    this.totalPages = totalPages;
+  }
+
+  public Long getTotalElements() {
+    return this.totalElements;
+  }
+
+  public void setTotalElements(final Long totalElements) {
+    this.totalElements = totalElements;
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/Meeting.java b/api/src/main/java/io/mifos/group/api/v1/domain/Meeting.java
new file mode 100644
index 0000000..7770ac3
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/Meeting.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import org.hibernate.validator.constraints.NotBlank;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+public class Meeting {
+
+  @NotBlank
+  private Integer meetingSequence;
+  @NotBlank
+  private String groupIdentifier;
+  @NotNull
+  private Integer currentCycle;
+  @NotEmpty
+  @Valid
+  private Set<Attendee> attendees;
+  private String scheduledFor;
+  @Valid
+  private Address location;
+  private String heldOn;
+  private Long duration;
+  private String createdOn;
+  private String createdBy;
+
+  public Meeting() {
+    super();
+  }
+
+  public Integer getMeetingSequence() {
+    return this.meetingSequence;
+  }
+
+  public void setMeetingSequence(final Integer meetingSequence) {
+    this.meetingSequence = meetingSequence;
+  }
+
+  public String getGroupIdentifier() {
+    return this.groupIdentifier;
+  }
+
+  public void setGroupIdentifier(final String groupIdentifier) {
+    this.groupIdentifier = groupIdentifier;
+  }
+
+  public Integer getCurrentCycle() {
+    return this.currentCycle;
+  }
+
+  public void setCurrentCycle(final Integer currentCycle) {
+    this.currentCycle = currentCycle;
+  }
+
+  public Set<Attendee> getAttendees() {
+    return this.attendees;
+  }
+
+  public void setAttendees(final Set<Attendee> attendees) {
+    this.attendees = attendees;
+  }
+
+  public String getScheduledFor() {
+    return this.scheduledFor;
+  }
+
+  public void setScheduledFor(final String scheduledFor) {
+    this.scheduledFor = scheduledFor;
+  }
+
+  public Address getLocation() {
+    return this.location;
+  }
+
+  public void setLocation(final Address location) {
+    this.location = location;
+  }
+
+  public String getHeldOn() {
+    return this.heldOn;
+  }
+
+  public void setHeldOn(final String heldOn) {
+    this.heldOn = heldOn;
+  }
+
+  public Long getDuration() {
+    return this.duration;
+  }
+
+  public void setDuration(final Long duration) {
+    this.duration = duration;
+  }
+
+  public String getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final String createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+}
diff --git a/api/src/main/java/io/mifos/group/api/v1/domain/SignOffMeeting.java b/api/src/main/java/io/mifos/group/api/v1/domain/SignOffMeeting.java
new file mode 100644
index 0000000..a8e71f7
--- /dev/null
+++ b/api/src/main/java/io/mifos/group/api/v1/domain/SignOffMeeting.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.api.v1.domain;
+
+import javax.validation.Valid;
+import java.util.Set;
+
+public class SignOffMeeting {
+
+  @Valid
+  private Integer cycle;
+  private Integer sequence;
+  private Set<Attendee> attendees;
+  private Long duration;
+
+  public SignOffMeeting() {
+    super();
+  }
+
+  public Integer getCycle() {
+    return this.cycle;
+  }
+
+  public void setCycle(final Integer cycle) {
+    this.cycle = cycle;
+  }
+
+  public Integer getSequence() {
+    return this.sequence;
+  }
+
+  public void setSequence(final Integer sequence) {
+    this.sequence = sequence;
+  }
+
+  public Set<Attendee> getAttendees() {
+    return this.attendees;
+  }
+
+  public void setAttendees(final Set<Attendee> attendees) {
+    this.attendees = attendees;
+  }
+
+  public Long getDuration() {
+    return this.duration;
+  }
+
+  public void setDuration(final Long duration) {
+    this.duration = duration;
+  }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..b8c09e6
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,29 @@
+group 'io.mifos'
+
+task publishApiToMavenLocal {
+    dependsOn gradle.includedBuild('api').task(':publishToMavenLocal')
+}
+
+task publishServiceToMavenLocal {
+    mustRunAfter publishApiToMavenLocal
+    dependsOn gradle.includedBuild('service').task(':publishToMavenLocal')
+}
+
+task publishComponentTestToMavenLocal {
+    mustRunAfter publishApiToMavenLocal
+    mustRunAfter publishServiceToMavenLocal
+    dependsOn gradle.includedBuild('component-test').task(':publishToMavenLocal')
+}
+
+task publishToMavenLocal {
+    group 'all'
+    dependsOn publishApiToMavenLocal
+    dependsOn publishServiceToMavenLocal
+    dependsOn publishComponentTestToMavenLocal
+}
+
+task prepareForTest {
+    group 'all'
+    dependsOn publishToMavenLocal
+    dependsOn gradle.includedBuild('component-test').task(':build')
+}
diff --git a/component-test/build.gradle b/component-test/build.gradle
new file mode 100644
index 0000000..b9a16bc
--- /dev/null
+++ b/component-test/build.gradle
@@ -0,0 +1,38 @@
+buildscript {
+    ext {
+        springBootVersion = '1.4.1.RELEASE'
+    }
+
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+    }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+dependencies {
+    compile(
+            [group: 'io.mifos.group', name: 'api', version: project.version],
+            [group: 'io.mifos.group', name: 'service', version: project.version],
+            [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
+            [group: 'io.mifos.core', name: 'test', version: versions.frameworktest],
+            [group: 'io.mifos.anubis', name: 'test', version: versions.frameworkanubis],
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+    )
+}
+
+publishing {
+    publications {
+        mavenJava(MavenPublication) {
+            from components.java
+        }
+    }
+}
diff --git a/component-test/settings.gradle b/component-test/settings.gradle
new file mode 100644
index 0000000..b2e36e3
--- /dev/null
+++ b/component-test/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'component-test'
diff --git a/component-test/src/main/java/io/mifos/group/TestGroup.java b/component-test/src/main/java/io/mifos/group/TestGroup.java
new file mode 100644
index 0000000..91110f9
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/TestGroup.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group;
+
+import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+import io.mifos.core.test.listener.EnableEventRecording;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.group.api.v1.EventConstants;
+import io.mifos.group.api.v1.client.GroupClient;
+import io.mifos.group.api.v1.domain.AssignedEmployeeHolder;
+import io.mifos.group.api.v1.domain.Attendee;
+import io.mifos.group.api.v1.domain.Group;
+import io.mifos.group.api.v1.domain.GroupCommand;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import io.mifos.group.api.v1.domain.Meeting;
+import io.mifos.group.api.v1.domain.SignOffMeeting;
+import io.mifos.group.service.GroupConfiguration;
+import io.mifos.group.util.GroupDefinitionGenerator;
+import io.mifos.group.util.GroupGenerator;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.time.Clock;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestGroup {
+  private static final String APP_NAME = "group-v1";
+  private static final String TEST_USER = "ranefer";
+
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+  @Autowired
+  private GroupClient groupClient;
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  public TestGroup() {
+    super();
+  }
+
+  @Before
+  public void prepTest() {
+    userContext = this.tenantApplicationSecurityEnvironment.createAutoUserContext(TestGroup.TEST_USER);
+  }
+
+  @After
+  public void cleanTest() {
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(EventConstants.INITIALIZE, EventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldCreateGroup() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final Group randomGroup = GroupGenerator.createRandomGroup(randomGroupDefinition.getIdentifier());
+
+    this.groupClient.createGroup(randomGroup);
+
+    this.eventRecorder.wait(EventConstants.POST_GROUP, randomGroup.getIdentifier());
+
+    final Group fetchedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals(randomGroup.getIdentifier(), fetchedGroup.getIdentifier());
+    Assert.assertEquals(randomGroup.getGroupDefinitionIdentifier(), fetchedGroup.getGroupDefinitionIdentifier());
+    Assert.assertEquals(randomGroup.getName(), fetchedGroup.getName());
+    Assert.assertEquals(randomGroup.getOffice(), fetchedGroup.getOffice());
+    Assert.assertEquals(randomGroup.getAssignedEmployee(), fetchedGroup.getAssignedEmployee());
+    Assert.assertEquals(randomGroup.getWeekday(), fetchedGroup.getWeekday());
+    Assert.assertEquals(Group.Status.PENDING.name(), fetchedGroup.getStatus());
+    Assert.assertEquals(randomGroup.getLeaders().size(), fetchedGroup.getLeaders().size());
+    Assert.assertEquals(randomGroup.getMembers().size(), fetchedGroup.getMembers().size());
+    Assert.assertNotNull(fetchedGroup.getCreatedBy());
+    Assert.assertNotNull(fetchedGroup.getCreatedOn());
+    Assert.assertNull(fetchedGroup.getLastModifiedBy());
+    Assert.assertNull(fetchedGroup.getLastModifiedOn());
+    Assert.assertNotNull(fetchedGroup.getAddress());
+  }
+
+  @Test
+  public void shouldActivateCommand() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final Group randomGroup = GroupGenerator.createRandomGroup(randomGroupDefinition.getIdentifier());
+    this.groupClient.createGroup(randomGroup);
+    this.eventRecorder.wait(EventConstants.POST_GROUP, randomGroup.getIdentifier());
+
+    final Group fetchedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals(Group.Status.PENDING.name(), fetchedGroup.getStatus());
+
+    final GroupCommand activate = new GroupCommand();
+    activate.setAction(GroupCommand.Action.ACTIVATE.name());
+    activate.setNote(RandomStringUtils.randomAlphanumeric(256));
+    activate.setCreatedBy(TestGroup.TEST_USER);
+    activate.setCreatedOn(ZonedDateTime.now(Clock.systemUTC()).format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
+
+    this.groupClient.processGroupCommand(randomGroup.getIdentifier(), activate);
+    this.eventRecorder.wait(EventConstants.ACTIVATE_GROUP, randomGroup.getIdentifier());
+
+    final Group activatedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals(Group.Status.ACTIVE.name(), activatedGroup.getStatus());
+
+    final List<GroupCommand> groupCommands = this.groupClient.fetchGroupCommands(activatedGroup.getIdentifier());
+    Assert.assertTrue(groupCommands.size() == 1);
+    final GroupCommand groupCommand = groupCommands.get(0);
+    Assert.assertEquals(activate.getAction(), groupCommand.getAction());
+    Assert.assertEquals(activate.getNote(), groupCommand.getNote());
+    Assert.assertEquals(activate.getCreatedBy(), groupCommand.getCreatedBy());
+    Assert.assertNotNull(groupCommand.getCreatedOn());
+
+    final List<Meeting> meetings = this.groupClient.fetchMeetings(activatedGroup.getIdentifier(), Boolean.FALSE);
+    Assert.assertNotNull(meetings);
+    Assert.assertEquals(randomGroupDefinition.getCycle().getNumberOfMeetings(), Integer.valueOf(meetings.size()));
+
+    final Meeting meeting2signOff = meetings.get(0);
+    final SignOffMeeting signOffMeeting = new SignOffMeeting();
+    signOffMeeting.setCycle(meeting2signOff.getCurrentCycle());
+    signOffMeeting.setSequence(meeting2signOff.getMeetingSequence());
+    signOffMeeting.setDuration(120L);
+    signOffMeeting.setAttendees(meeting2signOff.getAttendees()
+        .stream()
+        .map(attendee -> {
+          attendee.setStatus(Attendee.Status.ATTENDED.name());
+          return attendee;
+        })
+        .collect(Collectors.toSet())
+    );
+
+    this.groupClient.closeMeeting(activatedGroup.getIdentifier(), signOffMeeting);
+    this.eventRecorder.wait(EventConstants.PUT_GROUP, activatedGroup.getIdentifier());
+  }
+
+  @Test
+  public void shouldUpdateLeaders() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final Group randomGroup = GroupGenerator.createRandomGroup(randomGroupDefinition.getIdentifier());
+    this.groupClient.createGroup(randomGroup);
+    this.eventRecorder.wait(EventConstants.POST_GROUP, randomGroup.getIdentifier());
+
+    final int currentLeadersSize = randomGroup.getLeaders().size();
+    randomGroup.getLeaders().add(RandomStringUtils.randomAlphanumeric(32));
+    this.groupClient.updateLeaders(randomGroup.getIdentifier(), randomGroup.getLeaders());
+    this.eventRecorder.wait(EventConstants.PUT_GROUP, randomGroup.getIdentifier());
+
+    final Group fetchedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals((currentLeadersSize + 1), fetchedGroup.getLeaders().size());
+  }
+
+  @Test
+  public void shouldUpdateMembers() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final Group randomGroup = GroupGenerator.createRandomGroup(randomGroupDefinition.getIdentifier());
+    this.groupClient.createGroup(randomGroup);
+    this.eventRecorder.wait(EventConstants.POST_GROUP, randomGroup.getIdentifier());
+
+    final int currentMembersSize = randomGroup.getMembers().size();
+    randomGroup.getMembers().addAll(Arrays.asList(
+        RandomStringUtils.randomAlphanumeric(32),
+        RandomStringUtils.randomAlphanumeric(32)
+    ));
+    this.groupClient.updateMembers(randomGroup.getIdentifier(), randomGroup.getMembers());
+    this.eventRecorder.wait(EventConstants.PUT_GROUP, randomGroup.getIdentifier());
+
+    final Group fetchedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals((currentMembersSize + 2), fetchedGroup.getMembers().size());
+  }
+
+  @Test
+  public void shouldUpdateAssignedEmployee() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final Group randomGroup = GroupGenerator.createRandomGroup(randomGroupDefinition.getIdentifier());
+    this.groupClient.createGroup(randomGroup);
+    this.eventRecorder.wait(EventConstants.POST_GROUP, randomGroup.getIdentifier());
+
+    final AssignedEmployeeHolder anotherEmployee = new AssignedEmployeeHolder();
+    anotherEmployee.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+
+    this.groupClient.updateAssignedEmployee(randomGroup.getIdentifier(), anotherEmployee);
+    this.eventRecorder.wait(EventConstants.PUT_GROUP, randomGroup.getIdentifier());
+
+    final Group fetchedGroup = this.groupClient.findGroup(randomGroup.getIdentifier());
+    Assert.assertEquals(anotherEmployee.getIdentifier(), fetchedGroup.getAssignedEmployee());
+  }
+
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.group.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @Import({GroupConfiguration.class})
+  @ComponentScan("io.mifos.group.listener")
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+
+    @Bean()
+    public Logger logger() {
+      return LoggerFactory.getLogger("test-logger");
+    }
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/TestGroupDefinition.java b/component-test/src/main/java/io/mifos/group/TestGroupDefinition.java
new file mode 100644
index 0000000..b33ad72
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/TestGroupDefinition.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group;
+
+import io.mifos.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import io.mifos.core.api.context.AutoUserContext;
+import io.mifos.core.lang.TenantContextHolder;
+import io.mifos.core.test.env.TestEnvironment;
+import io.mifos.core.test.fixture.TenantDataStoreContextTestRule;
+import io.mifos.core.test.fixture.cassandra.CassandraInitializer;
+import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
+import io.mifos.core.test.listener.EnableEventRecording;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.group.api.v1.EventConstants;
+import io.mifos.group.api.v1.client.GroupClient;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import io.mifos.group.service.GroupConfiguration;
+import io.mifos.group.util.GroupDefinitionGenerator;
+import org.junit.*;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.netflix.feign.EnableFeignClients;
+import org.springframework.cloud.netflix.ribbon.RibbonClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+public class TestGroupDefinition {
+  private static final String APP_NAME = "group-v1";
+  private static final String TEST_USER = "ranefer";
+
+  private final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  private final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+  private final static MariaDBInitializer mariaDBInitializer = new MariaDBInitializer();
+  private final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer, mariaDBInitializer);
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+          .outerRule(testEnvironment)
+          .around(cassandraInitializer)
+          .around(mariaDBInitializer)
+          .around(tenantDataStoreContext);
+
+  @Rule
+  public final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment
+          = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment, this::waitForInitialize);
+
+  @Autowired
+  private GroupClient groupClient;
+  @Autowired
+  private EventRecorder eventRecorder;
+
+  private AutoUserContext userContext;
+
+  public TestGroupDefinition() {
+    super();
+  }
+
+  @Before
+  public void prepTest() {
+    userContext = this.tenantApplicationSecurityEnvironment.createAutoUserContext(TestGroupDefinition.TEST_USER);
+  }
+
+  @After
+  public void cleanTest() {
+    TenantContextHolder.clear();
+    userContext.close();
+  }
+
+  public boolean waitForInitialize() {
+    try {
+      return this.eventRecorder.wait(EventConstants.INITIALIZE, EventConstants.INITIALIZE);
+    } catch (final InterruptedException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Test
+  public void shouldCreateGroupDefinition() throws Exception {
+    final GroupDefinition randomGroupDefinition = GroupDefinitionGenerator.createRandomGroupDefinition();
+    this.groupClient.createGroupDefinition(randomGroupDefinition);
+
+    this.eventRecorder.wait(EventConstants.POST_GROUP_DEFINITION, randomGroupDefinition.getIdentifier());
+
+    final GroupDefinition fetchedGroupDefinition = this.groupClient.findGroupDefinition(randomGroupDefinition.getIdentifier());
+
+    Assert.assertEquals(randomGroupDefinition.getIdentifier(), fetchedGroupDefinition.getIdentifier());
+    Assert.assertEquals(randomGroupDefinition.getDescription(), fetchedGroupDefinition.getDescription());
+    Assert.assertEquals(randomGroupDefinition.getMinimalSize(), fetchedGroupDefinition.getMinimalSize());
+    Assert.assertEquals(randomGroupDefinition.getMaximalSize(), fetchedGroupDefinition.getMaximalSize());
+    Assert.assertNotNull(fetchedGroupDefinition.getCycle());
+    Assert.assertEquals(randomGroupDefinition.getCycle().getNumberOfMeetings(), fetchedGroupDefinition.getCycle().getNumberOfMeetings());
+    Assert.assertEquals(randomGroupDefinition.getCycle().getFrequency(), fetchedGroupDefinition.getCycle().getFrequency());
+    Assert.assertEquals(randomGroupDefinition.getCycle().getAdjustment(), fetchedGroupDefinition.getCycle().getAdjustment());
+    Assert.assertNotNull(fetchedGroupDefinition.getCreatedBy());
+    Assert.assertNotNull(fetchedGroupDefinition.getCreateOn());
+    Assert.assertNull(fetchedGroupDefinition.getLastModifiedBy());
+    Assert.assertNull(fetchedGroupDefinition.getLastModifiedOn());
+  }
+
+  @Configuration
+  @EnableEventRecording
+  @EnableFeignClients(basePackages = {"io.mifos.group.api.v1.client"})
+  @RibbonClient(name = APP_NAME)
+  @Import({GroupConfiguration.class})
+  @ComponentScan("io.mifos.group.listener")
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+
+    @Bean()
+    public Logger logger() {
+      return LoggerFactory.getLogger("test-logger");
+    }
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/listener/GroupDefinitionEventListener.java b/component-test/src/main/java/io/mifos/group/listener/GroupDefinitionEventListener.java
new file mode 100644
index 0000000..a64b355
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/listener/GroupDefinitionEventListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.group.api.v1.EventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@SuppressWarnings("unused")
+@Component
+public class GroupDefinitionEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public GroupDefinitionEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_POST_GROUP_DEFINITION
+  )
+  public void onGroupDefinitionCreated(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                       final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.POST_GROUP_DEFINITION, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/listener/GroupEventListener.java b/component-test/src/main/java/io/mifos/group/listener/GroupEventListener.java
new file mode 100644
index 0000000..32446b3
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/listener/GroupEventListener.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.group.api.v1.EventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@SuppressWarnings("unused")
+@Component
+public class GroupEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public GroupEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_POST_GROUP
+  )
+  public void onGroupCreated(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                             final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.POST_GROUP, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_ACTIVATE_GROUP
+  )
+  public void onGroupActivated(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                             final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.ACTIVATE_GROUP, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_PUT_GROUP
+  )
+  public void onGroupUpdated(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                               final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.PUT_GROUP, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/listener/MigrationEventListener.java b/component-test/src/main/java/io/mifos/group/listener/MigrationEventListener.java
new file mode 100644
index 0000000..df7c8fb
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/listener/MigrationEventListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.group.api.v1.EventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+@SuppressWarnings("unused")
+@Component
+public class MigrationEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public MigrationEventListener(final EventRecorder eventRecorder) {
+    super();
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_INITIALIZE
+  )
+  public void onInitialization(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                               final String payload) {
+    this.eventRecorder.event(tenant, EventConstants.INITIALIZE, payload, String.class);
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/util/GroupDefinitionGenerator.java b/component-test/src/main/java/io/mifos/group/util/GroupDefinitionGenerator.java
new file mode 100644
index 0000000..7024152
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/util/GroupDefinitionGenerator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.util;
+
+import io.mifos.group.api.v1.domain.Cycle;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public class GroupDefinitionGenerator {
+
+  private GroupDefinitionGenerator() {
+    super();
+  }
+
+  public static GroupDefinition createRandomGroupDefinition() {
+    final GroupDefinition groupDefinition = new GroupDefinition();
+    groupDefinition.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    groupDefinition.setDescription(RandomStringUtils.randomAlphabetic(2048));
+    groupDefinition.setMinimalSize(10);
+    groupDefinition.setMaximalSize(30);
+    final Cycle cycle = new Cycle();
+    cycle.setNumberOfMeetings(25);
+    cycle.setFrequency(Cycle.Frequency.WEEKLY.name());
+    cycle.setAdjustment(Cycle.Adjustment.NEXT_BUSINESS_DAY.name());
+    groupDefinition.setCycle(cycle);
+    return groupDefinition;
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/group/util/GroupGenerator.java b/component-test/src/main/java/io/mifos/group/util/GroupGenerator.java
new file mode 100644
index 0000000..d00bde4
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/group/util/GroupGenerator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 The Mifos Initiative
+ *
+ * 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 io.mifos.group.util;
+
+import io.mifos.group.api.v1.domain.Address;
+import io.mifos.group.api.v1.domain.Group;
+import org.apache.commons.lang3.RandomStringUtils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+public class GroupGenerator {
+
+  private GroupGenerator() {
+    super();
+  }
+
+  public static Group createRandomGroup(final String definitionIdentifier) {
+    final Group group = new Group();
+    group.setIdentifier(RandomStringUtils.randomAlphanumeric(32));
+    group.setGroupDefinitionIdentifier(definitionIdentifier);
+    group.setName(RandomStringUtils.randomAlphanumeric(256));
+    group.setOffice(RandomStringUtils.randomAlphanumeric(32));
+    group.setAssignedEmployee(RandomStringUtils.randomAlphanumeric(32));
+    group.setLeaders(new HashSet<>(Arrays.asList(
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32)
+    )));
+    group.setMembers(new HashSet<>(Arrays.asList(
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32),
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32),
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32),
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32),
+        RandomStringUtils.randomAlphanumeric(32), RandomStringUtils.randomAlphanumeric(32)
+    )));
+    group.setWeekday(Group.Weekday.WEDNESDAY.getValue());
+    final Address address = new Address();
+    address.setStreet(RandomStringUtils.randomAlphanumeric(256));
+    address.setCity(RandomStringUtils.randomAlphanumeric(256));
+    address.setRegion(RandomStringUtils.randomAlphanumeric(256));
+    address.setPostalCode(RandomStringUtils.randomAlphanumeric(2));
+    address.setCountry(RandomStringUtils.randomAlphanumeric(256));
+    address.setCountryCode(RandomStringUtils.randomAlphanumeric(2));
+    group.setAddress(address);
+    return group;
+  }
+}
diff --git a/component-test/src/main/resources/logback.xml b/component-test/src/main/resources/logback.xml
new file mode 100644
index 0000000..e0b1e5e
--- /dev/null
+++ b/component-test/src/main/resources/logback.xml
@@ -0,0 +1,28 @@
+<!--
+
+    Copyright 2017 The Mifos Initiative
+
+    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.
+
+-->
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="INFO">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..c733004
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4aca373
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Mar 16 14:47:20 CET 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4453cce
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save ( ) {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/service/build.gradle b/service/build.gradle
new file mode 100644
index 0000000..09e8a22
--- /dev/null
+++ b/service/build.gradle
@@ -0,0 +1,63 @@
+buildscript {
+    ext {
+        springBootVersion = '1.4.1.RELEASE'
+    }
+
+    repositories {
+        jcenter()
+    }
+
+    dependencies {
+        classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+    }
+}
+
+plugins {
+    id "com.github.hierynomus.license" version "0.13.1"
+}
+
+apply from: '../shared.gradle'
+
+apply plugin: 'spring-boot'
+
+springBoot {
+    executable = true
+    classifier = 'boot'
+}
+
+dependencies {
+    compile(
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-config'],
+            [group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka'],
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'],
+            [group: 'io.mifos.group', name: 'api', version: project.version],
+            [group: 'io.mifos.anubis', name: 'library', version: versions.frameworkanubis],
+            [group: 'com.google.code.gson', name: 'gson'],
+            [group: 'io.mifos.core', name: 'lang', version: versions.frameworklang],
+            [group: 'io.mifos.core', name: 'async', version: versions.frameworkasync],
+            [group: 'io.mifos.core', name: 'cassandra', version: versions.frameworkcassandra],
+            [group: 'io.mifos.core', name: 'mariadb', version: versions.frameworkmariadb],
+            [group: 'io.mifos.core', name: 'command', version: versions.frameworkcommand],
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+    )
+}
+
+publishToMavenLocal.dependsOn bootRepackage
+
+publishing {
+    publications {
+        service(MavenPublication) {
+            from components.java
+            groupId project.group
+            artifactId project.name
+            version project.version
+        }
+        bootService(MavenPublication) {
+            // "boot" jar
+            artifact ("$buildDir/libs/$project.name-$version-boot.jar")
+            groupId project.group
+            artifactId ("$project.name-boot")
+            version project.version
+        }
+    }
+}
diff --git a/service/settings.gradle b/service/settings.gradle
new file mode 100644
index 0000000..1ed471d
--- /dev/null
+++ b/service/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'service'
diff --git a/service/src/main/java/io/mifos/group/service/GroupApplication.java b/service/src/main/java/io/mifos/group/service/GroupApplication.java
new file mode 100644
index 0000000..1dac37d
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/GroupApplication.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service;
+
+import org.springframework.boot.SpringApplication;
+
+public class GroupApplication {
+
+  public GroupApplication() {
+    super();
+  }
+
+  public static void main(String[] args) {
+    SpringApplication.run(GroupConfiguration.class, args);
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/GroupConfiguration.java b/service/src/main/java/io/mifos/group/service/GroupConfiguration.java
new file mode 100644
index 0000000..0ecceeb
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/GroupConfiguration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service;
+
+import io.mifos.anubis.config.EnableAnubis;
+import io.mifos.core.async.config.EnableAsync;
+import io.mifos.core.cassandra.config.EnableCassandra;
+import io.mifos.core.command.config.EnableCommandProcessing;
+import io.mifos.core.lang.config.EnableServiceException;
+import io.mifos.core.lang.config.EnableTenantContext;
+import io.mifos.core.mariadb.config.EnableMariaDB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SuppressWarnings("WeakerAccess")
+@Configuration
+@EnableAutoConfiguration
+@EnableDiscoveryClient
+@EnableAsync
+@EnableTenantContext
+@EnableCassandra
+@EnableMariaDB
+@EnableCommandProcessing
+@EnableAnubis
+@EnableServiceException
+@ComponentScan({
+    "io.mifos.group.service.rest",
+    "io.mifos.group.service.internal.service",
+    "io.mifos.group.service.internal.repository",
+    "io.mifos.group.service.internal.command.handler"
+})
+@EnableJpaRepositories({
+    "io.mifos.group.service.internal.repository"
+})
+public class GroupConfiguration {
+
+  public GroupConfiguration() {
+    super();
+  }
+
+  @Bean(name = ServiceConstants.LOGGER_NAME)
+  public Logger logger() {
+    return LoggerFactory.getLogger(ServiceConstants.LOGGER_NAME);
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/ServiceConstants.java b/service/src/main/java/io/mifos/group/service/ServiceConstants.java
new file mode 100644
index 0000000..07ae9a0
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/ServiceConstants.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service;
+
+public interface ServiceConstants {
+  String LOGGER_NAME = "group-logger";
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/ActivateGroupCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/ActivateGroupCommand.java
new file mode 100644
index 0000000..6e03951
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/ActivateGroupCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.GroupCommand;
+
+public class ActivateGroupCommand {
+
+  private final String identifier;
+  private final GroupCommand groupCommand;
+
+  public ActivateGroupCommand(final String identifier, final GroupCommand groupCommand) {
+    super();
+    this.identifier = identifier;
+    this.groupCommand = groupCommand;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public GroupCommand groupCommand() {
+    return this.groupCommand;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/CloseGroupCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/CloseGroupCommand.java
new file mode 100644
index 0000000..e599458
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/CloseGroupCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.GroupCommand;
+
+public class CloseGroupCommand {
+
+  private final String identifier;
+  private final GroupCommand groupCommand;
+
+  public CloseGroupCommand(final String identifier, final GroupCommand groupCommand) {
+    super();
+    this.identifier = identifier;
+    this.groupCommand = groupCommand;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public GroupCommand groupCommand() {
+    return this.groupCommand;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupCommand.java
new file mode 100644
index 0000000..88515db
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupCommand.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.Group;
+
+public class CreateGroupCommand {
+
+  private final Group group;
+
+  public CreateGroupCommand(final Group group) {
+    super();
+    this.group = group;
+  }
+
+  public Group group() {
+    return this.group;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupDefinitionCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupDefinitionCommand.java
new file mode 100644
index 0000000..5c85f8d
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/CreateGroupDefinitionCommand.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.GroupDefinition;
+
+public class CreateGroupDefinitionCommand {
+
+  private final GroupDefinition groupDefinition;
+
+  public CreateGroupDefinitionCommand(final GroupDefinition groupDefinition) {
+    super();
+    this.groupDefinition = groupDefinition;
+  }
+
+  public GroupDefinition groupDefinition() {
+    return this.groupDefinition;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/InitializeServiceCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/InitializeServiceCommand.java
new file mode 100644
index 0000000..a5e28da
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/InitializeServiceCommand.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+public class InitializeServiceCommand {
+
+  public InitializeServiceCommand() {
+    super();
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/ReopenGroupCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/ReopenGroupCommand.java
new file mode 100644
index 0000000..0e83570
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/ReopenGroupCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.GroupCommand;
+
+public class ReopenGroupCommand {
+
+  private final String identifier;
+  private final GroupCommand groupCommand;
+
+  public ReopenGroupCommand(final String identifier, final GroupCommand groupCommand) {
+    super();
+    this.identifier = identifier;
+    this.groupCommand = groupCommand;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public GroupCommand groupCommand() {
+    return this.groupCommand;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/SignOffMeetingCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/SignOffMeetingCommand.java
new file mode 100644
index 0000000..0144b32
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/SignOffMeetingCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import io.mifos.group.api.v1.domain.SignOffMeeting;
+
+public class SignOffMeetingCommand {
+
+  private final String groupIdentifier;
+  private final SignOffMeeting signOffMeeting;
+
+  public SignOffMeetingCommand(final String groupIdentifier, final SignOffMeeting signOffMeeting) {
+    super();
+    this.groupIdentifier = groupIdentifier;
+    this.signOffMeeting = signOffMeeting;
+  }
+
+  public String groupIdentifier() {
+    return this.groupIdentifier;
+  }
+
+  public SignOffMeeting signOffMeeting() {
+    return this.signOffMeeting;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/UpdateAssignedEmployeeCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/UpdateAssignedEmployeeCommand.java
new file mode 100644
index 0000000..e664ac1
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/UpdateAssignedEmployeeCommand.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+public class UpdateAssignedEmployeeCommand {
+
+  private final String identifier;
+  private final String employeeIdentifier;
+
+  public UpdateAssignedEmployeeCommand(final String identifier, final String employeeIdentifier) {
+    super();
+    this.identifier = identifier;
+    this.employeeIdentifier = employeeIdentifier;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public String employeeIdentifier() {
+    return this.employeeIdentifier;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/UpdateLeadersCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/UpdateLeadersCommand.java
new file mode 100644
index 0000000..7167aeb
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/UpdateLeadersCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import java.util.Set;
+
+public class UpdateLeadersCommand {
+
+  private final String identifier;
+  private final Set<String> customerIdentifiers;
+
+  public UpdateLeadersCommand(final String identifier, final Set<String> customerIdentifiers) {
+    super();
+    this.identifier = identifier;
+    this.customerIdentifiers = customerIdentifiers;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public Set<String> customerIdentifiers() {
+    return this.customerIdentifiers;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/UpdateMembersCommand.java b/service/src/main/java/io/mifos/group/service/internal/command/UpdateMembersCommand.java
new file mode 100644
index 0000000..935257a
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/UpdateMembersCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command;
+
+import java.util.Set;
+
+public class UpdateMembersCommand {
+
+  private final String identifier;
+  private final Set<String> customerIdentifiers;
+
+  public UpdateMembersCommand(final String identifier, final Set<String> customerIdentifiers) {
+    super();
+    this.identifier = identifier;
+    this.customerIdentifiers = customerIdentifiers;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public Set<String> customerIdentifiers() {
+    return this.customerIdentifiers;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/handler/GroupAggregate.java b/service/src/main/java/io/mifos/group/service/internal/command/handler/GroupAggregate.java
new file mode 100644
index 0000000..49074ce
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/handler/GroupAggregate.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command.handler;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.group.service.internal.command.ActivateGroupCommand;
+import io.mifos.group.service.internal.command.CloseGroupCommand;
+import io.mifos.group.service.internal.command.CreateGroupCommand;
+import io.mifos.group.service.internal.command.CreateGroupDefinitionCommand;
+import io.mifos.group.service.internal.command.ReopenGroupCommand;
+import io.mifos.group.service.internal.command.SignOffMeetingCommand;
+import io.mifos.group.service.internal.command.UpdateAssignedEmployeeCommand;
+import io.mifos.group.service.internal.command.UpdateLeadersCommand;
+import io.mifos.group.service.internal.command.UpdateMembersCommand;
+import io.mifos.group.service.internal.repository.AddressEntity;
+import io.mifos.group.service.internal.repository.AddressRepository;
+import io.mifos.group.service.internal.repository.AttendeeEntity;
+import io.mifos.group.service.internal.repository.AttendeeRepository;
+import io.mifos.group.service.internal.repository.GroupCommandEntity;
+import io.mifos.group.service.internal.repository.GroupCommandRepository;
+import io.mifos.group.service.internal.repository.GroupDefinitionEntity;
+import io.mifos.group.service.internal.repository.GroupDefinitionRepository;
+import io.mifos.group.service.internal.repository.GroupEntity;
+import io.mifos.group.service.internal.repository.GroupRepository;
+import io.mifos.group.service.internal.repository.MeetingEntity;
+import io.mifos.group.service.internal.repository.MeetingRepository;
+import io.mifos.group.api.v1.EventConstants;
+import io.mifos.group.api.v1.domain.*;
+import io.mifos.group.service.internal.mapper.AddressMapper;
+import io.mifos.group.service.internal.mapper.GroupCommandMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.Clock;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoField;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("unused")
+@Aggregate
+public class GroupAggregate {
+
+  private final GroupDefinitionRepository groupDefinitionRepository;
+  private final GroupRepository groupRepository;
+  private final GroupCommandRepository groupCommandRepository;
+  private final MeetingRepository meetingRepository;
+  private final AttendeeRepository attendeeRepository;
+  private final AddressRepository addressRepository;
+
+  @Autowired
+  public GroupAggregate(final GroupDefinitionRepository groupDefinitionRepository,
+                        final GroupRepository groupRepository,
+                        final GroupCommandRepository groupCommandRepository,
+                        final MeetingRepository meetingRepository,
+                        final AttendeeRepository attendeeRepository,
+                        final AddressRepository addressRepository) {
+    super();
+    this.groupDefinitionRepository = groupDefinitionRepository;
+    this.groupRepository = groupRepository;
+    this.groupCommandRepository = groupCommandRepository;
+    this.meetingRepository = meetingRepository;
+    this.attendeeRepository = attendeeRepository;
+    this.addressRepository = addressRepository;
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_GROUP_DEFINITION)
+  public String createDefinition(final CreateGroupDefinitionCommand createGroupDefinitionCommand) {
+    final GroupDefinition groupDefinition = createGroupDefinitionCommand.groupDefinition();
+    final GroupDefinitionEntity groupDefinitionEntity = new GroupDefinitionEntity();
+    groupDefinitionEntity.setIdentifier(groupDefinition.getIdentifier());
+    groupDefinitionEntity.setDescription(groupDefinition.getDescription());
+    groupDefinitionEntity.setMinimalSize(groupDefinition.getMinimalSize());
+    groupDefinitionEntity.setMaximalSize(groupDefinition.getMaximalSize());
+    final Cycle cycle = groupDefinition.getCycle();
+    groupDefinitionEntity.setNumberOfMeetings(cycle.getNumberOfMeetings());
+    groupDefinitionEntity.setFrequency(cycle.getFrequency());
+    groupDefinitionEntity.setAdjustment(cycle.getAdjustment());
+    groupDefinitionEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    groupDefinitionEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    this.groupDefinitionRepository.save(groupDefinitionEntity);
+
+    return groupDefinition.getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_GROUP)
+  public String createGroup(final CreateGroupCommand createGroupCommand) {
+    final Group group = createGroupCommand.group();
+    final GroupDefinitionEntity groupDefinitionEntity =
+        this.groupDefinitionRepository.findByIdentifier(group.getGroupDefinitionIdentifier())
+            .orElseThrow(
+                () -> ServiceException.notFound("Group definition {0} not found.", group.getGroupDefinitionIdentifier())
+            );
+
+    final AddressEntity savedAddress = this.addressRepository.save(AddressMapper.map(group.getAddress()));
+
+    final GroupEntity groupEntity = new GroupEntity();
+    groupEntity.setGroupDefinition(groupDefinitionEntity);
+    groupEntity.setIdentifier(group.getIdentifier());
+    groupEntity.setName(group.getName());
+    groupEntity.setLeaders(StringUtils.collectionToCommaDelimitedString(group.getLeaders()));
+    groupEntity.setMembers(StringUtils.collectionToCommaDelimitedString(group.getMembers()));
+    groupEntity.setOffice(group.getOffice());
+    groupEntity.setAddressEntity(savedAddress);
+    groupEntity.setAssignedEmployee(group.getAssignedEmployee());
+    groupEntity.setWeekday(group.getWeekday());
+    groupEntity.setGroupStatus(Group.Status.PENDING.name());
+    groupEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    groupEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    this.groupRepository.save(groupEntity);
+    return group.getIdentifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.ACTIVATE_GROUP)
+  public String activateGroup(final ActivateGroupCommand activateGroupCommand) {
+    this.groupRepository.findByIdentifier(activateGroupCommand.identifier())
+        .ifPresent(groupEntity -> {
+          final GroupEntity savedGroupEntity = this.processCommandInternally(groupEntity, activateGroupCommand.groupCommand());
+          this.createMeetingSchedule(groupEntity.getGroupDefinition(), savedGroupEntity);
+        });
+    return activateGroupCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.CLOSE_GROUP)
+  public String closeGroup(final CloseGroupCommand closeGroupCommand) {
+    this.groupRepository.findByIdentifier(closeGroupCommand.identifier())
+        .ifPresent(groupEntity -> {
+          final List<MeetingEntity> currentMeetings =
+              this.meetingRepository.findByGroupEntityAndCurrentCycleOrderByMeetingSequenceDesc(groupEntity, groupEntity.getCurrentCycle());
+          if (currentMeetings.stream().anyMatch(meetingEntity -> meetingEntity.getHeldOn() == null)) {
+            throw ServiceException.conflict("Not all meetings for group {0} signed off.", closeGroupCommand.identifier());
+          }
+          this.processCommandInternally(groupEntity, closeGroupCommand.groupCommand());
+        });
+    return closeGroupCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.REOPEN_GROUP)
+  public String reopenGroup(final ReopenGroupCommand reopenGroupCommand) {
+    this.groupRepository.findByIdentifier(reopenGroupCommand.identifier())
+        .ifPresent(groupEntity -> {
+          final GroupEntity savedGroupEntity = this.processCommandInternally(groupEntity, reopenGroupCommand.groupCommand());
+          this.createMeetingSchedule(groupEntity.getGroupDefinition(), savedGroupEntity);
+        });
+    return reopenGroupCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_GROUP)
+  public String updateLeaders(final UpdateLeadersCommand updateLeadersCommand) {
+    this.groupRepository.findByIdentifier(updateLeadersCommand.identifier())
+        .ifPresent(groupEntity -> {
+          groupEntity.setLeaders(StringUtils.collectionToCommaDelimitedString(updateLeadersCommand.customerIdentifiers()));
+          groupEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+          groupEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+          this.groupRepository.save(groupEntity);
+        });
+    return updateLeadersCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_GROUP)
+  public String updateMembers(final UpdateMembersCommand updateMembersCommand) {
+    this.groupRepository.findByIdentifier(updateMembersCommand.identifier())
+        .ifPresent(groupEntity -> {
+          groupEntity.setMembers(StringUtils.collectionToCommaDelimitedString(updateMembersCommand.customerIdentifiers()));
+          groupEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+          groupEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+          this.groupRepository.save(groupEntity);
+        });
+    return updateMembersCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_GROUP)
+  public String updateAssignedEmployee(final UpdateAssignedEmployeeCommand updateAssignedEmployeeCommand) {
+    this.groupRepository.findByIdentifier(updateAssignedEmployeeCommand.identifier())
+        .ifPresent(groupEntity -> {
+          groupEntity.setAssignedEmployee(updateAssignedEmployeeCommand.employeeIdentifier());
+          groupEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+          groupEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+          this.groupRepository.save(groupEntity);
+        });
+    return updateAssignedEmployeeCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_GROUP)
+  public String signOffMeeting(final SignOffMeetingCommand signOffMeetingCommand) {
+    this.groupRepository.findByIdentifier(signOffMeetingCommand.groupIdentifier())
+        .ifPresent(groupEntity -> {
+          final SignOffMeeting signOffMeeting = signOffMeetingCommand.signOffMeeting();
+          this.meetingRepository
+              .findByGroupEntityAndCurrentCycleAndMeetingSequence(groupEntity,
+                  signOffMeeting.getCycle(), signOffMeeting.getSequence())
+              .ifPresent(meetingEntity -> {
+                meetingEntity.setDuration(signOffMeeting.getDuration());
+                meetingEntity.setHeldOn(LocalDate.now(Clock.systemUTC()));
+                this.meetingRepository.save(meetingEntity);
+
+                final List<AttendeeEntity> attendeeEntities = this.attendeeRepository.findByMeeting(meetingEntity);
+                attendeeEntities.forEach(attendeeEntity -> signOffMeeting.getAttendees()
+                    .stream()
+                    .filter(attendee -> attendee.getCustomerIdentifier().equals(attendeeEntity.getCustomerIdentifier()))
+                    .findFirst()
+                    .ifPresent(attendee -> {
+                      attendeeEntity.setStatus(attendee.getStatus());
+                      this.attendeeRepository.save(attendeeEntity);
+                }));
+              });
+        });
+    return signOffMeetingCommand.groupIdentifier();
+  }
+
+  private void createMeetingSchedule(final GroupDefinitionEntity groupDefinitionEntity, final GroupEntity groupEntity) {
+    final Integer numberOfMeetings = groupDefinitionEntity.getNumberOfMeetings();
+    final Cycle.Frequency frequency = Cycle.Frequency.valueOf(groupDefinitionEntity.getFrequency());
+    final Cycle.Adjustment adjustment = Cycle.Adjustment.valueOf(groupDefinitionEntity.getAdjustment());
+    final Group.Weekday weekday = Group.Weekday.from(groupEntity.getWeekday());
+
+    final Set<String> members = StringUtils.commaDelimitedListToSet(groupEntity.getMembers());
+
+    LocalDate meeting = LocalDate.now(Clock.systemUTC());
+    if (frequency != Cycle.Frequency.DAILY) {
+      meeting = meeting.with(ChronoField.DAY_OF_WEEK, weekday.getValue());
+    }
+
+    for (int i = 0; i < numberOfMeetings; i++) {
+      switch (frequency) {
+        case DAILY:
+          meeting = meeting.plusDays(1L);
+          break;
+        case WEEKLY:
+          meeting = meeting.plusWeeks(1L);
+          break;
+        case FORTNIGHTLY:
+          meeting = meeting.plusWeeks(2L);
+          break;
+        case MONTHLY:
+          meeting.plusMonths(1L);
+          break;
+      }
+
+      final MeetingEntity meetingEntity = new MeetingEntity();
+      meetingEntity.setGroupEntity(groupEntity);
+      meetingEntity.setCurrentCycle(groupEntity.getCurrentCycle());
+      meetingEntity.setMeetingSequence((i + 1));
+      meetingEntity.setScheduledFor(meeting);
+      meetingEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+      meetingEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+      final MeetingEntity savedMeeting = this.meetingRepository.save(meetingEntity);
+
+      this.attendeeRepository.save(
+          members
+              .stream()
+              .map(member -> {
+                final AttendeeEntity attendeeEntity = new AttendeeEntity();
+                attendeeEntity.setMeeting(savedMeeting);
+                attendeeEntity.setCustomerIdentifier(member);
+                attendeeEntity.setStatus(Attendee.Status.EXPECTED.name());
+                return attendeeEntity;
+              })
+              .collect(Collectors.toList())
+      );
+    }
+  }
+
+  private GroupEntity processCommandInternally(final GroupEntity groupEntity, final GroupCommand groupCommand) {
+    this.saveGroupCommand(groupEntity, groupCommand);
+
+    final GroupCommand.Action action = GroupCommand.Action.valueOf(groupCommand.getAction());
+    switch (action) {
+      case ACTIVATE:
+        groupEntity.setGroupStatus(Group.Status.ACTIVE.name());
+        groupEntity.setCurrentCycle(groupEntity.getCurrentCycle() + 1);
+        break;
+      case CLOSE:
+        groupEntity.setGroupStatus(Group.Status.CLOSED.name());
+        break;
+      case REOPEN:
+        groupEntity.setGroupStatus(Group.Status.PENDING.name());
+        groupEntity.setCurrentCycle(groupEntity.getCurrentCycle() + 1);
+        break;
+      default:
+        throw ServiceException.badRequest("Unsupported command {}.", action.name());
+    }
+    groupEntity.setLastModifiedBy(groupCommand.getCreatedBy());
+    groupEntity.setLastModifiedOn(DateConverter.fromIsoString(groupCommand.getCreatedOn()));
+    return this.groupRepository.save(groupEntity);
+  }
+
+  private void saveGroupCommand(final GroupEntity groupEntity, final GroupCommand groupCommand) {
+    final GroupCommandEntity groupCommandEntity = GroupCommandMapper.map(groupCommand);
+    groupCommandEntity.setGroup(groupEntity);
+    this.groupCommandRepository.save(groupCommandEntity);
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/command/handler/MigrationAggregate.java b/service/src/main/java/io/mifos/group/service/internal/command/handler/MigrationAggregate.java
new file mode 100644
index 0000000..3d93d3b
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/command/handler/MigrationAggregate.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.command.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.mariadb.domain.FlywayFactoryBean;
+import io.mifos.group.api.v1.EventConstants;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.command.InitializeServiceCommand;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+
+@SuppressWarnings("unused")
+@Aggregate
+public class MigrationAggregate {
+
+  private final Logger logger;
+  private final DataSource dataSource;
+  private final FlywayFactoryBean flywayFactoryBean;
+
+  @Autowired
+  public MigrationAggregate(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                            final DataSource dataSource,
+                            final FlywayFactoryBean flywayFactoryBean) {
+    super();
+    this.logger = logger;
+    this.dataSource = dataSource;
+    this.flywayFactoryBean = flywayFactoryBean;
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.INITIALIZE)
+  public String initialize(final InitializeServiceCommand initializeServiceCommand) {
+    this.logger.debug("Start service migration.");
+    this.flywayFactoryBean.create(this.dataSource).migrate();
+    return EventConstants.INITIALIZE;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/AddressMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/AddressMapper.java
new file mode 100644
index 0000000..7ff897a
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/AddressMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.group.service.internal.repository.AddressEntity;
+import io.mifos.group.api.v1.domain.Address;
+
+public class AddressMapper {
+
+  private AddressMapper() {
+    super();
+  }
+
+  public static Address map(final AddressEntity addressEntity) {
+    final Address address = new Address();
+    address.setStreet(addressEntity.getStreet());
+    address.setCity(addressEntity.getCity());
+    address.setRegion(addressEntity.getRegion());
+    address.setPostalCode(addressEntity.getPostalCode());
+    address.setCountry(addressEntity.getCountry());
+    address.setCountryCode(addressEntity.getCountryCode());
+    return address;
+  }
+
+  public static AddressEntity map(final Address address) {
+    final AddressEntity addressEntity = new AddressEntity();
+    addressEntity.setStreet(address.getStreet());
+    addressEntity.setCity(address.getCity());
+    addressEntity.setRegion(address.getRegion());
+    addressEntity.setPostalCode(address.getPostalCode());
+    addressEntity.setCountry(address.getCountry());
+    addressEntity.setCountryCode(address.getCountryCode());
+    return addressEntity;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/AttendeeMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/AttendeeMapper.java
new file mode 100644
index 0000000..58cc7bb
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/AttendeeMapper.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.group.api.v1.domain.Attendee;
+import io.mifos.group.service.internal.repository.AttendeeEntity;
+
+public class AttendeeMapper {
+
+  private AttendeeMapper() {
+    super();
+  }
+
+  public static Attendee map(final AttendeeEntity attendeeEntity) {
+    final Attendee attendee = new Attendee();
+    attendee.setCustomerIdentifier(attendeeEntity.getCustomerIdentifier());
+    attendee.setStatus(attendeeEntity.getStatus());
+    return attendee;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/GroupCommandMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupCommandMapper.java
new file mode 100644
index 0000000..c1bbca0
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupCommandMapper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.group.service.internal.repository.GroupCommandEntity;
+import io.mifos.group.api.v1.domain.GroupCommand;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+
+public class GroupCommandMapper {
+
+  private GroupCommandMapper() {
+    super();
+  }
+
+  public static GroupCommand map(final GroupCommandEntity groupCommandEntity) {
+    final GroupCommand groupCommand = new GroupCommand();
+    groupCommand.setAction(groupCommandEntity.getAction());
+    groupCommand.setNote(groupCommandEntity.getNote());
+    groupCommand.setCreatedBy(groupCommandEntity.getCreatedBy());
+    groupCommand.setCreatedOn(DateConverter.toIsoString(groupCommandEntity.getCreatedOn()));
+    return groupCommand;
+  }
+
+  public static GroupCommandEntity map(final GroupCommand groupCommand) {
+    final GroupCommandEntity groupCommandEntity = new GroupCommandEntity();
+    groupCommandEntity.setAction(groupCommand.getAction());
+    groupCommandEntity.setNote(groupCommand.getNote());
+    groupCommandEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+    groupCommandEntity.setCreatedOn(LocalDateTime.now(Clock.systemUTC()));
+    return groupCommandEntity;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/GroupDefinitionMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupDefinitionMapper.java
new file mode 100644
index 0000000..88cf2c4
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupDefinitionMapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.core.lang.DateConverter;
+import io.mifos.group.service.internal.repository.GroupDefinitionEntity;
+import io.mifos.group.api.v1.domain.Cycle;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+
+public class GroupDefinitionMapper {
+
+  private GroupDefinitionMapper() {
+    super();
+  }
+
+  public static GroupDefinition map(final GroupDefinitionEntity groupDefinitionEntity) {
+    final GroupDefinition groupDefinition = new GroupDefinition();
+    groupDefinition.setIdentifier(groupDefinitionEntity.getIdentifier());
+    groupDefinition.setDescription(groupDefinitionEntity.getDescription());
+    groupDefinition.setMinimalSize(groupDefinitionEntity.getMinimalSize());
+    groupDefinition.setMaximalSize(groupDefinitionEntity.getMaximalSize());
+    groupDefinition.setCreateOn(DateConverter.toIsoString(groupDefinitionEntity.getCreatedOn()));
+    groupDefinition.setCreatedBy(groupDefinitionEntity.getCreatedBy());
+
+    if (groupDefinitionEntity.getLastModifiedOn() != null) {
+      groupDefinition.setLastModifiedOn(DateConverter.toIsoString(groupDefinitionEntity.getLastModifiedOn()));
+      groupDefinition.setLastModifiedBy(groupDefinitionEntity.getLastModifiedBy());
+    }
+
+    final Cycle cycle = new Cycle();
+    cycle.setNumberOfMeetings(groupDefinitionEntity.getNumberOfMeetings());
+    cycle.setFrequency(groupDefinitionEntity.getFrequency());
+    cycle.setAdjustment(groupDefinitionEntity.getAdjustment());
+    groupDefinition.setCycle(cycle);
+
+    return groupDefinition;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/GroupMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupMapper.java
new file mode 100644
index 0000000..a345c2a
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/GroupMapper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.core.lang.DateConverter;
+import io.mifos.group.service.internal.repository.GroupEntity;
+import io.mifos.group.api.v1.domain.Group;
+import org.springframework.util.StringUtils;
+
+public class GroupMapper {
+
+  private GroupMapper() {
+    super();
+  }
+
+  public static Group map(final GroupEntity groupEntity) {
+    final Group group = new Group();
+    group.setIdentifier(groupEntity.getIdentifier());
+    group.setGroupDefinitionIdentifier(groupEntity.getGroupDefinition().getIdentifier());
+    group.setName(groupEntity.getName());
+    group.setLeaders(StringUtils.commaDelimitedListToSet(groupEntity.getLeaders()));
+    group.setMembers(StringUtils.commaDelimitedListToSet(groupEntity.getMembers()));
+    group.setOffice(groupEntity.getOffice());
+    group.setAssignedEmployee(groupEntity.getAssignedEmployee());
+    group.setWeekday(groupEntity.getWeekday());
+    group.setStatus(groupEntity.getGroupStatus());
+    group.setCreatedOn(DateConverter.toIsoString(groupEntity.getCreatedOn()));
+    group.setCreatedBy(groupEntity.getCreatedBy());
+    if (groupEntity.getLastModifiedOn() != null) {
+      group.setLastModifiedOn(DateConverter.toIsoString(groupEntity.getLastModifiedOn()));
+      group.setLastModifiedBy(groupEntity.getLastModifiedBy());
+    }
+    return group;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/mapper/MeetingMapper.java b/service/src/main/java/io/mifos/group/service/internal/mapper/MeetingMapper.java
new file mode 100644
index 0000000..9e67548
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/mapper/MeetingMapper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.mapper;
+
+import io.mifos.core.lang.DateConverter;
+import io.mifos.group.api.v1.domain.Meeting;
+import io.mifos.group.service.internal.repository.MeetingEntity;
+
+public class MeetingMapper {
+
+  private MeetingMapper() {
+    super();
+  }
+
+  public static Meeting map(final MeetingEntity meetingEntity) {
+    final Meeting meeting = new Meeting();
+    meeting.setCurrentCycle(meetingEntity.getCurrentCycle());
+    meeting.setMeetingSequence(meetingEntity.getMeetingSequence());
+    meeting.setScheduledFor(DateConverter.toIsoString(meetingEntity.getScheduledFor()));
+    if (meetingEntity.getHeldOn() != null) {
+      meeting.setHeldOn(DateConverter.toIsoString(meetingEntity.getHeldOn()));
+    }
+    if (meetingEntity.getCreatedBy() != null) {
+      meeting.setCreatedBy(meetingEntity.getCreatedBy());
+      meeting.setCreatedOn(DateConverter.toIsoString(meetingEntity.getCreatedOn()));
+    }
+    meeting.setDuration(meetingEntity.getDuration());
+    return meeting;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/AddressEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/AddressEntity.java
new file mode 100644
index 0000000..f456da2
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/AddressEntity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "ptah_addresses")
+public class AddressEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+  @Column(name = "street")
+  private String street;
+  @Column(name = "city")
+  private String city;
+  @Column(name = "postal_code")
+  private String postalCode;
+  @Column(name = "region")
+  private String region;
+  @Column(name = "country_code")
+  private String countryCode;
+  @Column(name = "country")
+  private String country;
+
+  public AddressEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getStreet() {
+    return this.street;
+  }
+
+  public void setStreet(final String street) {
+    this.street = street;
+  }
+
+  public String getCity() {
+    return this.city;
+  }
+
+  public void setCity(final String city) {
+    this.city = city;
+  }
+
+  public String getPostalCode() {
+    return this.postalCode;
+  }
+
+  public void setPostalCode(final String postalCode) {
+    this.postalCode = postalCode;
+  }
+
+  public String getRegion() {
+    return this.region;
+  }
+
+  public void setRegion(final String region) {
+    this.region = region;
+  }
+
+  public String getCountryCode() {
+    return this.countryCode;
+  }
+
+  public void setCountryCode(final String countryCode) {
+    this.countryCode = countryCode;
+  }
+
+  public String getCountry() {
+    return this.country;
+  }
+
+  public void setCountry(final String country) {
+    this.country = country;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/AddressRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/AddressRepository.java
new file mode 100644
index 0000000..b3dfdfb
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/AddressRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface AddressRepository extends JpaRepository<AddressEntity, Long> {
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeEntity.java
new file mode 100644
index 0000000..f0b37db
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeEntity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "ptah_attendees")
+public class AttendeeEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "meeting_id", nullable = false)
+  private MeetingEntity meeting;
+  @Column(name = "customer_identifier", nullable = false)
+  private String customerIdentifier;
+  @Column(name = "a_status", nullable = false)
+  private String status;
+
+  public AttendeeEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public MeetingEntity getMeeting() {
+    return this.meeting;
+  }
+
+  public void setMeeting(final MeetingEntity meeting) {
+    this.meeting = meeting;
+  }
+
+  public String getCustomerIdentifier() {
+    return this.customerIdentifier;
+  }
+
+  public void setCustomerIdentifier(final String customerIdentifier) {
+    this.customerIdentifier = customerIdentifier;
+  }
+
+  public String getStatus() {
+    return this.status;
+  }
+
+  public void setStatus(final String status) {
+    this.status = status;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeRepository.java
new file mode 100644
index 0000000..b718aa6
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/AttendeeRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface AttendeeRepository extends JpaRepository<AttendeeEntity, Long> {
+
+  List<AttendeeEntity> findByMeeting(final MeetingEntity meetingEntity);
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandEntity.java
new file mode 100644
index 0000000..332c02e
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandEntity.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "ptah_group_commands")
+public class GroupCommandEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "group_id", nullable = false)
+  private GroupEntity group;
+  @Column(name = "a_action")
+  private String action;
+  @Column(name = "note")
+  private String note;
+  @Column(name = "created_on", nullable = false)
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "created_by", nullable = false)
+  private String createdBy;
+
+  public GroupCommandEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public GroupEntity getGroup() {
+    return this.group;
+  }
+
+  public void setGroup(final GroupEntity group) {
+    this.group = group;
+  }
+
+  public String getAction() {
+    return this.action;
+  }
+
+  public void setAction(final String action) {
+    this.action = action;
+  }
+
+  public String getNote() {
+    return this.note;
+  }
+
+  public void setNote(final String note) {
+    this.note = note;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandRepository.java
new file mode 100644
index 0000000..9313514
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupCommandRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface GroupCommandRepository extends JpaRepository<GroupCommandEntity, Long> {
+
+  List<GroupCommandEntity> findByGroup(final GroupEntity group);
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionEntity.java
new file mode 100644
index 0000000..926e832
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionEntity.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "ptah_group_definitions")
+public class GroupDefinitionEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @Column(name = "identifier", nullable = false)
+  private String identifier;
+  @Column(name = "description")
+  private String description;
+  @Column(name = "minimal_size", nullable = false)
+  private Integer minimalSize;
+  @Column(name = "maximal_size", nullable = false)
+  private Integer maximalSize;
+  @Column(name = "number_of_meetings", nullable = false)
+  private Integer numberOfMeetings;
+  @Column(name = "frequency", nullable = false)
+  private String frequency;
+  @Column(name = "adjustment")
+  private String adjustment;
+  @Column(name = "created_on", nullable = false)
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "created_by", nullable = false)
+  private String createdBy;
+  @Column(name = "last_modified_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime lastModifiedOn;
+  @Column(name = "last_modified_by")
+  private String lastModifiedBy;
+
+  public GroupDefinitionEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public Integer getMinimalSize() {
+    return this.minimalSize;
+  }
+
+  public void setMinimalSize(final Integer minimalSize) {
+    this.minimalSize = minimalSize;
+  }
+
+  public Integer getMaximalSize() {
+    return this.maximalSize;
+  }
+
+  public void setMaximalSize(final Integer maximalSize) {
+    this.maximalSize = maximalSize;
+  }
+
+  public Integer getNumberOfMeetings() {
+    return this.numberOfMeetings;
+  }
+
+  public void setNumberOfMeetings(final Integer numberOfMeetings) {
+    this.numberOfMeetings = numberOfMeetings;
+  }
+
+  public String getFrequency() {
+    return this.frequency;
+  }
+
+  public void setFrequency(final String frequency) {
+    this.frequency = frequency;
+  }
+
+  public String getAdjustment() {
+    return this.adjustment;
+  }
+
+  public void setAdjustment(final String adjustment) {
+    this.adjustment = adjustment;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final LocalDateTime lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionRepository.java
new file mode 100644
index 0000000..e274b95
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupDefinitionRepository.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface GroupDefinitionRepository extends JpaRepository<GroupDefinitionEntity, Long> {
+
+  Optional<GroupDefinitionEntity> findByIdentifier(final String identifier);
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupEntity.java
new file mode 100644
index 0000000..da5a423
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupEntity.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "ptah_groups")
+public class GroupEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @Column(name = "identifier", nullable = false)
+  private String identifier;
+  @ManyToOne(fetch = FetchType.EAGER)
+  @JoinColumn(name = "group_definition_id", nullable = false)
+  private GroupDefinitionEntity groupDefinition;
+  @Column(name = "a_name", nullable = false)
+  private String name;
+  @Column(name = "leaders")
+  private String leaders;
+  @Column(name = "members", nullable = false)
+  private String members;
+  @Column(name = "office", nullable = false)
+  private String office;
+  @OneToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "address_id", nullable = false)
+  private AddressEntity addressEntity;
+  @Column(name = "assigned_employee", nullable = false)
+  private String assignedEmployee;
+  @Column(name = "weekday", nullable = false)
+  private Integer weekday;
+  @Column(name = "group_status", nullable = false)
+  private String groupStatus;
+  @Column(name = "current_cycle", nullable = false)
+  private Integer currentCycle = Integer.valueOf(0);
+  @Column(name = "created_on", nullable = false)
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "created_by", nullable = false)
+  private String createdBy;
+  @Column(name = "last_modified_on")
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime lastModifiedOn;
+  @Column(name = "last_modified_by")
+  private String lastModifiedBy;
+
+  public GroupEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public String getIdentifier() {
+    return this.identifier;
+  }
+
+  public void setIdentifier(final String identifier) {
+    this.identifier = identifier;
+  }
+
+  public GroupDefinitionEntity getGroupDefinition() {
+    return this.groupDefinition;
+  }
+
+  public void setGroupDefinition(final GroupDefinitionEntity groupDefinition) {
+    this.groupDefinition = groupDefinition;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getLeaders() {
+    return this.leaders;
+  }
+
+  public void setLeaders(final String leaders) {
+    this.leaders = leaders;
+  }
+
+  public String getMembers() {
+    return this.members;
+  }
+
+  public void setMembers(final String members) {
+    this.members = members;
+  }
+
+  public String getOffice() {
+    return this.office;
+  }
+
+  public void setOffice(final String office) {
+    this.office = office;
+  }
+
+  public String getAssignedEmployee() {
+    return this.assignedEmployee;
+  }
+
+  public void setAssignedEmployee(final String assignedEmployee) {
+    this.assignedEmployee = assignedEmployee;
+  }
+
+  public AddressEntity getAddressEntity() {
+    return this.addressEntity;
+  }
+
+  public void setAddressEntity(final AddressEntity addressEntity) {
+    this.addressEntity = addressEntity;
+  }
+
+  public Integer getWeekday() {
+    return this.weekday;
+  }
+
+  public void setWeekday(final Integer weekday) {
+    this.weekday = weekday;
+  }
+
+  public String getGroupStatus() {
+    return this.groupStatus;
+  }
+
+  public void setGroupStatus(final String groupStatus) {
+    this.groupStatus = groupStatus;
+  }
+
+  public Integer getCurrentCycle() {
+    return this.currentCycle;
+  }
+
+  public void setCurrentCycle(final Integer currentCycle) {
+    this.currentCycle = currentCycle;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+
+  public LocalDateTime getLastModifiedOn() {
+    return this.lastModifiedOn;
+  }
+
+  public void setLastModifiedOn(final LocalDateTime lastModifiedOn) {
+    this.lastModifiedOn = lastModifiedOn;
+  }
+
+  public String getLastModifiedBy() {
+    return this.lastModifiedBy;
+  }
+
+  public void setLastModifiedBy(final String lastModifiedBy) {
+    this.lastModifiedBy = lastModifiedBy;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/GroupRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/GroupRepository.java
new file mode 100644
index 0000000..7398865
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/GroupRepository.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface GroupRepository extends JpaRepository<GroupEntity, Long> {
+
+  Optional<GroupEntity> findByIdentifier(final String identifier);
+
+  Page<GroupEntity> findByAssignedEmployee(final String employee, final Pageable pageable);
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/MeetingEntity.java b/service/src/main/java/io/mifos/group/service/internal/repository/MeetingEntity.java
new file mode 100644
index 0000000..2733c5a
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/MeetingEntity.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateConverter;
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.Column;
+import javax.persistence.Convert;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "ptah_meetings")
+public class MeetingEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id", nullable = false)
+  private Long id;
+  @Column(name = "meeting_sequence", nullable = false)
+  private Integer meetingSequence;
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "group_id", nullable = false)
+  private GroupEntity groupEntity;
+  @Column(name = "current_cycle", nullable = false)
+  private Integer currentCycle;
+  @Column(name = "scheduled_for", nullable = false)
+  @Convert(converter = LocalDateConverter.class)
+  private LocalDate scheduledFor;
+  @Column(name = "held_on", nullable = true)
+  @Convert(converter = LocalDateConverter.class)
+  private LocalDate heldOn;
+  @Column(name = "duration", nullable = true)
+  private Long duration;
+  @Column(name = "created_on", nullable = false)
+  @Convert(converter = LocalDateTimeConverter.class)
+  private LocalDateTime createdOn;
+  @Column(name = "created_by", nullable = false)
+  private String createdBy;
+
+  public MeetingEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public Integer getMeetingSequence() {
+    return this.meetingSequence;
+  }
+
+  public void setMeetingSequence(final Integer meetingSequence) {
+    this.meetingSequence = meetingSequence;
+  }
+
+  public GroupEntity getGroupEntity() {
+    return this.groupEntity;
+  }
+
+  public void setGroupEntity(final GroupEntity groupEntity) {
+    this.groupEntity = groupEntity;
+  }
+
+  public Integer getCurrentCycle() {
+    return this.currentCycle;
+  }
+
+  public void setCurrentCycle(final Integer currentCycle) {
+    this.currentCycle = currentCycle;
+  }
+
+  public LocalDate getScheduledFor() {
+    return this.scheduledFor;
+  }
+
+  public void setScheduledFor(final LocalDate scheduledFor) {
+    this.scheduledFor = scheduledFor;
+  }
+
+  public LocalDate getHeldOn() {
+    return this.heldOn;
+  }
+
+  public void setHeldOn(final LocalDate heldOn) {
+    this.heldOn = heldOn;
+  }
+
+  public Long getDuration() {
+    return this.duration;
+  }
+
+  public void setDuration(final Long duration) {
+    this.duration = duration;
+  }
+
+  public LocalDateTime getCreatedOn() {
+    return this.createdOn;
+  }
+
+  public void setCreatedOn(final LocalDateTime createdOn) {
+    this.createdOn = createdOn;
+  }
+
+  public String getCreatedBy() {
+    return this.createdBy;
+  }
+
+  public void setCreatedBy(final String createdBy) {
+    this.createdBy = createdBy;
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/repository/MeetingRepository.java b/service/src/main/java/io/mifos/group/service/internal/repository/MeetingRepository.java
new file mode 100644
index 0000000..6875ba7
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/repository/MeetingRepository.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateConverter;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.Convert;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface MeetingRepository extends JpaRepository<MeetingEntity, Long> {
+
+  @Convert(converter = LocalDateConverter.class)
+  List<MeetingEntity> findTopByGroupEntityAndScheduledForAfter(final GroupEntity groupEntity, final LocalDate date);
+
+  List<MeetingEntity> findByGroupEntityAndCurrentCycleOrderByMeetingSequenceDesc(
+      final GroupEntity groupEntity, final Integer currentCycle);
+
+  List<MeetingEntity> findByGroupEntityOrderByCurrentCycleDescMeetingSequenceDesc(final GroupEntity groupEntity);
+
+  Optional<MeetingEntity> findByGroupEntityAndCurrentCycleAndMeetingSequence(final GroupEntity groupEntity, final Integer cycle, final Integer sequence);
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/service/GroupDefinitionService.java b/service/src/main/java/io/mifos/group/service/internal/service/GroupDefinitionService.java
new file mode 100644
index 0000000..2bc03cb
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/service/GroupDefinitionService.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.service;
+
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.mapper.GroupDefinitionMapper;
+import io.mifos.group.service.internal.repository.GroupDefinitionRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class GroupDefinitionService {
+
+  private final Logger logger;
+  private final GroupDefinitionRepository groupDefinitionRepository;
+
+  @Autowired
+  public GroupDefinitionService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                                final GroupDefinitionRepository groupDefinitionRepository) {
+    super();
+    this.logger = logger;
+    this.groupDefinitionRepository = groupDefinitionRepository;
+  }
+
+  public Optional<GroupDefinition> findByIdentifier(final String identifier) {
+    return this.groupDefinitionRepository.findByIdentifier(identifier).map(GroupDefinitionMapper::map);
+  }
+
+  public List<GroupDefinition> fetchAllGroupDefinitions() {
+    return this.groupDefinitionRepository.findAll()
+        .stream()
+        .map(GroupDefinitionMapper::map)
+        .collect(Collectors.toList());
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/internal/service/GroupService.java b/service/src/main/java/io/mifos/group/service/internal/service/GroupService.java
new file mode 100644
index 0000000..76ce37e
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/internal/service/GroupService.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.internal.service;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.group.service.internal.repository.AttendeeRepository;
+import io.mifos.group.service.internal.repository.GroupRepository;
+import io.mifos.group.api.v1.domain.Group;
+import io.mifos.group.api.v1.domain.GroupCommand;
+import io.mifos.group.api.v1.domain.GroupPage;
+import io.mifos.group.api.v1.domain.Meeting;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.mapper.AddressMapper;
+import io.mifos.group.service.internal.mapper.AttendeeMapper;
+import io.mifos.group.service.internal.mapper.GroupCommandMapper;
+import io.mifos.group.service.internal.mapper.GroupMapper;
+import io.mifos.group.service.internal.mapper.MeetingMapper;
+import io.mifos.group.service.internal.repository.GroupCommandRepository;
+import io.mifos.group.service.internal.repository.GroupEntity;
+import io.mifos.group.service.internal.repository.MeetingEntity;
+import io.mifos.group.service.internal.repository.MeetingRepository;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Service
+public class GroupService {
+
+  private final Logger logger;
+  private final GroupRepository groupRepository;
+  private final GroupCommandRepository groupCommandRepository;
+  private final MeetingRepository meetingRepository;
+  private final AttendeeRepository attendeeRepository;
+
+  @Autowired
+  public GroupService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                      final GroupRepository groupRepository,
+                      final GroupCommandRepository groupCommandRepository,
+                      final MeetingRepository meetingRepository,
+                      final AttendeeRepository attendeeRepository) {
+    super();
+    this.logger = logger;
+    this.groupRepository = groupRepository;
+    this.groupCommandRepository = groupCommandRepository;
+    this.meetingRepository = meetingRepository;
+    this.attendeeRepository = attendeeRepository;
+  }
+
+  public Optional<Group> findByIdentifier(final String identifier) {
+    final Optional<GroupEntity> optionalGroup = this.groupRepository.findByIdentifier(identifier);
+    if (optionalGroup.isPresent()) {
+      final GroupEntity groupEntity = optionalGroup.get();
+      final Group group = GroupMapper.map(groupEntity);
+      group.setAddress(AddressMapper.map(groupEntity.getAddressEntity()));
+      return Optional.of(group);
+    } else {
+      return Optional.empty();
+    }
+  }
+
+  public GroupPage fetchGroups(final String employee, final Pageable pageable) {
+    final Page<GroupEntity> page;
+    if (employee != null) {
+      page = this.groupRepository.findByAssignedEmployee(employee, pageable);
+    } else {
+      page = this.groupRepository.findAll(pageable);
+    }
+
+    final GroupPage groupPage = new GroupPage();
+    groupPage.setGroups(page.map(GroupMapper::map).getContent());
+    groupPage.setTotalPages(page.getTotalPages());
+    groupPage.setTotalElements(page.getTotalElements());
+
+    return groupPage;
+  }
+
+  public List<GroupCommand> findCommandsByIdentifier(final String identifier) {
+    final GroupEntity groupEntity =
+        this.groupRepository.findByIdentifier(identifier)
+            .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+    return this.groupCommandRepository.findByGroup(groupEntity)
+        .stream()
+        .map(GroupCommandMapper::map)
+        .collect(Collectors.toList());
+  }
+
+  public List<Meeting> findMeetings(final String identifier, final Boolean upcoming) {
+    final GroupEntity groupEntity = this.groupRepository.findByIdentifier(identifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+
+    final List<MeetingEntity> meetings;
+    if (upcoming) {
+      meetings = this.meetingRepository.findTopByGroupEntityAndScheduledForAfter(groupEntity, LocalDate.now(Clock.systemUTC()));
+    } else {
+      meetings = this.meetingRepository.findByGroupEntityOrderByCurrentCycleDescMeetingSequenceDesc(groupEntity);
+    }
+
+    return meetings
+        .stream()
+        .map(meetingEntity -> {
+          final Meeting meeting = MeetingMapper.map(meetingEntity);
+          meeting.setGroupIdentifier(groupEntity.getIdentifier());
+          meeting.setAttendees(
+              this.attendeeRepository.findByMeeting(meetingEntity)
+                  .stream().map(AttendeeMapper::map).collect(Collectors.toSet())
+          );
+          meeting.setLocation(AddressMapper.map(groupEntity.getAddressEntity()));
+          return meeting;
+        })
+        .collect(Collectors.toList());
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/rest/GroupDefinitionRestController.java b/service/src/main/java/io/mifos/group/service/rest/GroupDefinitionRestController.java
new file mode 100644
index 0000000..7b78586
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/rest/GroupDefinitionRestController.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.command.CreateGroupDefinitionCommand;
+import io.mifos.group.service.internal.service.GroupDefinitionService;
+import io.mifos.group.api.v1.domain.GroupDefinition;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping("/definitions")
+public class GroupDefinitionRestController {
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+  private final GroupDefinitionService groupDefinitionService;
+
+  @Autowired
+  public GroupDefinitionRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                                       final CommandGateway commandGateway,
+                                       final GroupDefinitionService groupDefinitionService) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+    this.groupDefinitionService = groupDefinitionService;
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      method = RequestMethod.POST,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> createDefinition(@RequestBody @Valid final GroupDefinition groupDefinition) {
+    this.groupDefinitionService.findByIdentifier(groupDefinition.getIdentifier())
+        .ifPresent(gd -> {
+          throw ServiceException.conflict("Group definition {0} already exists.", gd.getIdentifier());
+        });
+
+    this.commandGateway.process(new CreateGroupDefinitionCommand(groupDefinition));
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<List<GroupDefinition>> fetchAllGroupDefinitions() {
+    return ResponseEntity.ok(this.groupDefinitionService.fetchAllGroupDefinitions());
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      value = "/{identifier}",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<GroupDefinition> findGroupDefinitionByIdentifier(
+      @PathVariable("identifier") final String identifier) {
+    return this.groupDefinitionService.findByIdentifier(identifier)
+        .map(ResponseEntity::ok)
+        .orElseThrow(() -> ServiceException.notFound("Group definition {0} not found.", identifier));
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/rest/GroupRestController.java b/service/src/main/java/io/mifos/group/service/rest/GroupRestController.java
new file mode 100644
index 0000000..7aad78b
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/rest/GroupRestController.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.group.service.internal.command.SignOffMeetingCommand;
+import io.mifos.group.service.internal.command.UpdateLeadersCommand;
+import io.mifos.group.service.internal.command.UpdateMembersCommand;
+import io.mifos.group.service.internal.service.GroupDefinitionService;
+import io.mifos.group.service.internal.service.GroupService;
+import io.mifos.group.api.v1.domain.AssignedEmployeeHolder;
+import io.mifos.group.api.v1.domain.Group;
+import io.mifos.group.api.v1.domain.GroupCommand;
+import io.mifos.group.api.v1.domain.GroupPage;
+import io.mifos.group.api.v1.domain.Meeting;
+import io.mifos.group.api.v1.domain.SignOffMeeting;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.command.ActivateGroupCommand;
+import io.mifos.group.service.internal.command.CloseGroupCommand;
+import io.mifos.group.service.internal.command.CreateGroupCommand;
+import io.mifos.group.service.internal.command.ReopenGroupCommand;
+import io.mifos.group.service.internal.command.UpdateAssignedEmployeeCommand;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Set;
+
+@RestController
+@RequestMapping("/groups")
+public class GroupRestController {
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+  private final GroupService groupService;
+  private final GroupDefinitionService groupDefinitionService;
+
+
+  @Autowired
+  public GroupRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                             final CommandGateway commandGateway,
+                             final GroupService groupService,
+                             final GroupDefinitionService groupDefinitionService) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+    this.groupService = groupService;
+    this.groupDefinitionService = groupDefinitionService;
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      method = RequestMethod.POST,
+      consumes = MediaType.APPLICATION_JSON_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> createGroup(@RequestBody @Valid final Group group) {
+    this.groupService.findByIdentifier(group.getIdentifier())
+        .ifPresent(g -> {
+          throw ServiceException.conflict("Group {0} already exists.", g.getIdentifier());
+        });
+
+    if (!this.groupDefinitionService.findByIdentifier(group.getGroupDefinitionIdentifier()).isPresent()) {
+      throw ServiceException.notFound("Unknown group definition {0}.", group.getGroupDefinitionIdentifier());
+    }
+
+    this.commandGateway.process(new CreateGroupCommand(group));
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<GroupPage> fetchGroups(
+      @RequestParam("employee") final String employee,
+      @RequestParam("page") final Integer page,
+      @RequestParam("size") final Integer size,
+      @RequestParam("sortColumn") final String sortColumn,
+      @RequestParam("sortDirection") final String sortDirection) {
+    return ResponseEntity.ok(
+        this.groupService.fetchGroups(employee, this.createPageRequest(page, size, sortColumn, sortDirection))
+    );
+  }
+
+  @Permittable(AcceptedTokenType.TENANT)
+  @RequestMapping(
+      value = "/{identifier}",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Group> findByIdentifier(@PathVariable("identifier") final String identifier) {
+    return this.groupService.findByIdentifier(identifier)
+        .map(ResponseEntity::ok)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/commands",
+      method = RequestMethod.POST,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> processGroupCommand(@PathVariable("identifier") final String identifier, @RequestBody final GroupCommand groupCommand) {
+    this.groupService.findByIdentifier(identifier).orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+    final GroupCommand.Action action = GroupCommand.Action.valueOf(groupCommand.getAction());
+    switch (action) {
+      case ACTIVATE:
+        this.commandGateway.process(new ActivateGroupCommand(identifier, groupCommand));
+        break;
+      case CLOSE:
+        this.commandGateway.process(new CloseGroupCommand(identifier, groupCommand));
+        break;
+      case REOPEN:
+        this.commandGateway.process(new ReopenGroupCommand(identifier, groupCommand));
+        break;
+      default:
+        throw ServiceException.badRequest("Unsupported command {0}.", action.name());
+    }
+    return ResponseEntity.accepted().build();
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/commands",
+      method = RequestMethod.GET,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<List<GroupCommand>> getGroupCommands(@PathVariable("identifier") final String identifier) {
+    return ResponseEntity.ok(this.groupService.findCommandsByIdentifier(identifier));
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/leaders",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> updateLeaders(@PathVariable("identifier") final String identifier,
+                                     @RequestBody final Set<String> customerIdentifiers) {
+    this.groupService.findByIdentifier(identifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+
+    this.commandGateway.process(new UpdateLeadersCommand(identifier, customerIdentifiers));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/members",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> updateMembers(@PathVariable("identifier") final String identifier,
+                                     @RequestBody final Set<String> customerIdentifiers) {
+    this.groupService.findByIdentifier(identifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+
+    this.commandGateway.process(new UpdateMembersCommand(identifier, customerIdentifiers));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/employee",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> updateAssignedEmployee(@PathVariable("identifier") final String identifier,
+                                              @RequestBody final AssignedEmployeeHolder assignedEmployeeHolder) {
+
+    this.groupService.findByIdentifier(identifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", identifier));
+
+    this.commandGateway.process(new UpdateAssignedEmployeeCommand(identifier, assignedEmployeeHolder.getIdentifier()));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/meetings",
+      method = RequestMethod.GET,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<List<Meeting>> fetchMeetings(@PathVariable("identifier") final String groupIdentifier,
+                                              @RequestParam(value = "upcoming", required = false, defaultValue = "false") final Boolean upcoming) {
+    this.groupService.findByIdentifier(groupIdentifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", groupIdentifier));
+    return ResponseEntity.ok(this.groupService.findMeetings(groupIdentifier, upcoming));
+  }
+
+  @RequestMapping(
+      value = "/{identifier}/meetings",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  ResponseEntity<Void> closeMeeting(@PathVariable("identifier") final String groupIdentifier,
+                                    @RequestBody final SignOffMeeting signOffMeeting) {
+    this.groupService.findByIdentifier(groupIdentifier)
+        .orElseThrow(() -> ServiceException.notFound("Group {0} not found.", groupIdentifier));
+
+    this.commandGateway.process(new SignOffMeetingCommand(groupIdentifier, signOffMeeting));
+    return ResponseEntity.accepted().build();
+  }
+
+  private Pageable createPageRequest(final Integer page, final Integer size, final String sortColumn, final String sortDirection) {
+    final Integer pageToUse = page != null ? page : 0;
+    final Integer sizeToUse = size != null ? size : 20;
+    final String sortColumnToUse = sortColumn != null ? sortColumn : "identifier";
+    final Sort.Direction direction = sortDirection != null ? Sort.Direction.valueOf(sortDirection.toUpperCase()) : Sort.Direction.ASC;
+    return new PageRequest(pageToUse, sizeToUse, direction, sortColumnToUse);
+  }
+}
diff --git a/service/src/main/java/io/mifos/group/service/rest/MigrationRestController.java b/service/src/main/java/io/mifos/group/service/rest/MigrationRestController.java
new file mode 100644
index 0000000..e656b2a
--- /dev/null
+++ b/service/src/main/java/io/mifos/group/service/rest/MigrationRestController.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 The Mifos Initiative.
+ *
+ * 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 io.mifos.group.service.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.group.service.ServiceConstants;
+import io.mifos.group.service.internal.command.InitializeServiceCommand;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/initialize")
+public class MigrationRestController {
+
+  private final Logger logger;
+  private final CommandGateway commandGateway;
+
+  @Autowired
+  public MigrationRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
+                                 final CommandGateway commandGateway) {
+    super();
+    this.logger = logger;
+    this.commandGateway = commandGateway;
+  }
+
+  @Permittable(AcceptedTokenType.SYSTEM)
+  @RequestMapping(
+      method = RequestMethod.POST,
+      consumes = MediaType.ALL_VALUE,
+      produces = MediaType.APPLICATION_JSON_VALUE
+  )
+  public
+  @ResponseBody
+  ResponseEntity<Void> initialize() throws InterruptedException {
+    this.commandGateway.process(new InitializeServiceCommand());
+    return ResponseEntity.accepted().build();
+  }
+}
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
new file mode 100644
index 0000000..fbbfa68
--- /dev/null
+++ b/service/src/main/resources/application.yml
@@ -0,0 +1,67 @@
+
+#
+# Copyright 2016 The Mifos Initiative.
+#
+# 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.
+#
+
+spring:
+  cloud:
+    discovery:
+      enabled: false
+    config:
+      enabled: false
+
+eureka:
+  client:
+    serviceUrl:
+      defaultZone: http://localhost:8761/eureka/
+
+server:
+  port: 8081
+  contextPath: /group/v1/*
+
+cassandra:
+  clusterName: staging_cluster
+  contactPoints: 127.0.0.1:9042,127.0.0.2:9042,127.0.0.3:9042
+  keyspace: seshat
+  cl:
+    read: LOCAL_QUORUM
+    write: LOCAL_QUORUM
+    delete: LOCAL_QUORUM
+
+mariadb:
+  driverClass: org.mariadb.jdbc.Driver
+  database: seshat
+  host: localhost
+  port: 3306
+  user: root
+  password: mysql
+
+bonecp:
+  idleMaxAgeInMinutes: 240
+  idleConnectionTestPeriodInMinutes: 60
+  maxConnectionsPerPartition: 10
+  minConnectionsPerPartition: 1
+  partitionCount: 2
+  acquireIncrement: 5
+  statementsCacheSize: 100
+
+async:
+  corePoolSize: 32
+  maxPoolSize: 16384
+  queueCapacity: 0
+  threadName: async-processor-
+
+flyway:
+  enabled: false
diff --git a/service/src/main/resources/bootstrap.yml b/service/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..93080e3
--- /dev/null
+++ b/service/src/main/resources/bootstrap.yml
@@ -0,0 +1,19 @@
+#
+# Copyright 2016 The Mifos Initiative.
+#
+# 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.
+#
+
+spring:
+    application:
+        name: group-api
diff --git a/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
new file mode 100644
index 0000000..ab45c4b
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V1__initial_setup.sql
@@ -0,0 +1,100 @@
+--
+-- Copyright 2016 The Mifos Initiative.
+--
+-- 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.
+--
+
+CREATE TABLE ptah_addresses (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  street       VARCHAR(256) NOT NULL,
+  city         VARCHAR(256) NOT NULL,
+  postal_code  VARCHAR(32) NULL,
+  region       VARCHAR(256) NULL,
+  country_code VARCHAR(2) NOT NULL,
+  country      VARCHAR(256) NOT NULL,
+  CONSTRAINT ptah_addresses_pk PRIMARY KEY (id)
+);
+
+CREATE TABLE ptah_group_definitions (
+  id                 BIGINT        NOT NULL AUTO_INCREMENT,
+  identifier         VARCHAR(32)   NOT NULL,
+  description        VARCHAR(2048) NULL,
+  minimal_size       INTEGER       NOT NULL,
+  maximal_size       INTEGER       NOT NULL,
+  number_of_meetings INTEGER       NOT NULL,
+  frequency          VARCHAR(32)   NOT NULL,
+  adjustment         VARCHAR(32)   NULL,
+  created_on         TIMESTAMP(3)  NOT NULL,
+  created_by         VARCHAR(32)   NOT NULL,
+  last_modified_on   TIMESTAMP(3)  NULL,
+  last_modified_by   VARCHAR(32)   NULL,
+  CONSTRAINT ptah_group_definitions_pk PRIMARY KEY (id),
+  CONSTRAINT ptah_group_definitions_identifier_uq UNIQUE (identifier)
+);
+
+CREATE TABLE ptah_groups (
+  id                  BIGINT        NOT NULL AUTO_INCREMENT,
+  identifier          VARCHAR(32)   NOT NULL,
+  group_definition_id BIGINT        NOT NULL,
+  a_name              VARCHAR(256)  NOT NULL,
+  leaders             VARCHAR(512)  NULL,
+  members             VARCHAR(2048) NOT NULL,
+  office              VARCHAR(32)   NOT NULL,
+  assigned_employee   VARCHAR(32)   NOT NULL,
+  weekday             INTEGER       NOT NULL,
+  group_status        VARCHAR(32)   NOT NULL,
+  current_cycle       BIGINT        NOT NULL ,
+  address_id          BIGINT        NOT NULL,
+  created_on          TIMESTAMP(3)  NOT NULL,
+  created_by          VARCHAR(32)   NOT NULL,
+  last_modified_on    TIMESTAMP(3)  NULL,
+  last_modified_by    VARCHAR(32)   NULL,
+  CONSTRAINT ptah_groups_pk PRIMARY KEY (id),
+  CONSTRAINT ptah_groups_identifier_uq UNIQUE (identifier),
+  CONSTRAINT ptah_groups_definitions_fk FOREIGN KEY (group_definition_id) REFERENCES ptah_group_definitions (id),
+  CONSTRAINT ptah_group_addresses_fk FOREIGN KEY (address_id) REFERENCES ptah_addresses (id)
+);
+
+CREATE TABLE ptah_group_commands (
+  id         BIGINT       NOT NULL AUTO_INCREMENT,
+  group_id   BIGINT       NOT NULL,
+  a_action   VARCHAR(32)  NOT NULL,
+  note       VARCHAR(256) NOT NULL,
+  created_on TIMESTAMP(3) NOT NULL,
+  created_by VARCHAR(32)  NOT NULL,
+  CONSTRAINT ptah_group_commands_pk PRIMARY KEY (id),
+  CONSTRAINT ptah_group_commands_definitions_fk FOREIGN KEY (group_id) REFERENCES ptah_groups (id)
+);
+
+CREATE TABLE ptah_meetings (
+  id               BIGINT       NOT NULL AUTO_INCREMENT,
+  group_id         BIGINT       NOT NULL,
+  meeting_sequence BIGINT       NOT NULL,
+  current_cycle    BIGINT       NOT NULL,
+  scheduled_for    DATE         NOT NULL,
+  held_on          DATE         NULL,
+  duration         BIGINT       NULL,
+  created_on       TIMESTAMP(3) NOT NULL,
+  created_by       VARCHAR(32)  NOT NULL,
+  CONSTRAINT ptah_meetings_pk PRIMARY KEY (id),
+  CONSTRAINT ptah_group_meetings_fk FOREIGN KEY (group_id) REFERENCES ptah_groups (id)
+);
+
+CREATE TABLE ptah_attendees (
+  id                  BIGINT       NOT NULL AUTO_INCREMENT,
+  meeting_id          BIGINT       NOT NULL,
+  customer_identifier VARCHAR(32)  NOT NULL,
+  a_status            VARCHAR(256) NOT NULL,
+  CONSTRAINT ptah_attendees_pk PRIMARY KEY (id),
+  CONSTRAINT ptah_meeting_attendees_fk FOREIGN KEY (meeting_id) REFERENCES ptah_attendees (id)
+);
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c82a6db
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+rootProject.name = 'group'
+
+includeBuild 'api'
+includeBuild 'service'
+includeBuild 'component-test'
diff --git a/shared.gradle b/shared.gradle
new file mode 100644
index 0000000..c54ad58
--- /dev/null
+++ b/shared.gradle
@@ -0,0 +1,64 @@
+group 'io.mifos.group'
+version '0.1.0-BUILD-SNAPSHOT'
+
+ext.versions = [
+        frameworkapi : '0.1.0-BUILD-SNAPSHOT',
+        frameworklang : '0.1.0-BUILD-SNAPSHOT',
+        frameworkasync : '0.1.0-BUILD-SNAPSHOT',
+        frameworkcassandra : '0.1.0-BUILD-SNAPSHOT',
+        frameworkmariadb : '0.1.0-BUILD-SNAPSHOT',
+        frameworkcommand : '0.1.0-BUILD-SNAPSHOT',
+        frameworktest: '0.1.0-BUILD-SNAPSHOT',
+        frameworkanubis: '0.1.0-BUILD-SNAPSHOT',
+        validator  : '5.3.0.Final'
+]
+
+apply plugin: 'java'
+apply plugin: 'idea'
+apply plugin: 'maven-publish'
+apply plugin: 'io.spring.dependency-management'
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+repositories {
+    jcenter()
+    mavenLocal()
+}
+
+dependencyManagement {
+    imports {
+        mavenBom 'io.spring.platform:platform-bom:Athens-RELEASE'
+        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.SR1'
+    }
+}
+
+// override certain dependency provided by Spring platform using newer releases
+ext['cassandra.version'] = '3.6'
+ext['cassandra-driver.version'] = '3.1.2'
+ext['activemq.version'] = '5.13.2'
+ext['spring-data-releasetrain.version'] = 'Gosling-SR2A'
+
+dependencies {
+    compile(
+            [group: 'com.google.code.findbugs', name: 'jsr305']
+    )
+
+    testCompile(
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test']
+    )
+}
+
+license {
+    header rootProject.file('../HEADER')
+    strictCheck true
+    mapping {
+        java = 'SLASHSTAR_STYLE'
+        xml = 'XML_STYLE'
+        yml = 'SCRIPT_STYLE'
+        yaml = 'SCRIPT_STYLE'
+        uxf = 'XML_STYLE'
+    }
+    ext.year = Calendar.getInstance().get(Calendar.YEAR)
+    ext.name = 'The Mifos Initiative'
+}