Repository: mainflux/mainflux
Pull request #1259 (2020-10-16 16:18:05)

MF-397 - Introduce Thing Groups
Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>

Adding group of things

codecov-io: # [Codecov](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@02db406`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `18.19%`.

[![Impacted file tree graph](https://codecov.io/gh/mainflux/mainflux/pull/1259/graphs/tree.svg?width=650&height=150&src=pr&token=L932pGLS8b)](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master    #1259   +/-   ##
=========================================
  Coverage          ?   64.17%           
=========================================
  Files             ?      109           
  Lines             ?     7846           
  Branches          ?        0           
=========================================
  Hits              ?     5035           
  Misses            ?     2371           
  Partials          ?      440           
```


| [Impacted Files](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [things/postgres/groups.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3Bvc3RncmVzL2dyb3Vwcy5nbw==) | `0.00% <0.00%> (ø)` | |
| [things/postgres/things.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3Bvc3RncmVzL3RoaW5ncy5nbw==) | `79.42% <ø> (ø)` | |
| [things/redis/streams.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3JlZGlzL3N0cmVhbXMuZ28=) | `85.59% <0.00%> (ø)` | |
| [things/service.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3NlcnZpY2UuZ28=) | `51.91% <1.61%> (ø)` | |
| [things/api/things/http/transport.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL2FwaS90aGluZ3MvaHR0cC90cmFuc3BvcnQuZ28=) | `90.02% <98.50%> (ø)` | |
| [things/postgres/channels.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3Bvc3RncmVzL2NoYW5uZWxzLmdv) | `79.76% <100.00%> (ø)` | |
| [things/postgres/init.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dGhpbmdzL3Bvc3RncmVzL2luaXQuZ28=) | `95.12% <100.00%> (ø)` | |
| [users/service.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-dXNlcnMvc2VydmljZS5nbw==) | `66.42% <100.00%> (ø)` | |
| [readers/influxdb/messages.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-cmVhZGVycy9pbmZsdXhkYi9tZXNzYWdlcy5nbw==) | `74.76% <0.00%> (ø)` | |
| [writers/postgres/messages.go](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree#diff-d3JpdGVycy9wb3N0Z3Jlcy9tZXNzYWdlcy5nbw==) | `52.83% <0.00%> (ø)` | |
| ... and [107 more](https://codecov.io/gh/mainflux/mainflux/pull/1259/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=footer). Last update [02db406...98bef89](https://codecov.io/gh/mainflux/mainflux/pull/1259?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/bootstrap/mocks/things.go b/bootstrap/mocks/things.go
index 26611764..fdb9e0cd 100644
--- a/bootstrap/mocks/things.go
+++ b/bootstrap/mocks/things.go
@@ -9,6 +9,7 @@ import (
 	"sync"
 
 	"github.com/mainflux/mainflux"
+	"github.com/mainflux/mainflux/internal/groups"
 	"github.com/mainflux/mainflux/things"
 )
 
@@ -230,3 +231,47 @@ func findIndex(list []string, val string) int {
 
 	return -1
 }
+
+func (svc *mainfluxThings) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) RemoveGroup(ctx context.Context, token, id string) error {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) Assign(ctx context.Context, token, memberID, groupID string) error {
+	panic("not implemented")
+}
+
+func (svc *mainfluxThings) Unassign(ctx context.Context, token, memberID, groupID string) error {
+	panic("not implemented")
+}
diff --git a/cmd/things/main.go b/cmd/things/main.go
index 1547429b..883ac0f1 100644
--- a/cmd/things/main.go
+++ b/cmd/things/main.go
@@ -310,6 +310,9 @@ func newService(auth mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, c
 	channelsRepo := postgres.NewChannelRepository(database)
 	channelsRepo = tracing.ChannelRepositoryMiddleware(dbTracer, channelsRepo)
 
+	groupsRepo := postgres.NewGroupRepo(database)
+	groupsRepo = tracing.GroupRepositoryMiddleware(dbTracer, groupsRepo)
+
 	chanCache := rediscache.NewChannelCache(cacheClient)
 	chanCache = tracing.ChannelCacheMiddleware(cacheTracer, chanCache)
 
@@ -317,7 +320,7 @@ func newService(auth mainflux.AuthNServiceClient, dbTracer opentracing.Tracer, c
 	thingCache = tracing.ThingCacheMiddleware(cacheTracer, thingCache)
 	up := uuidProvider.New()
 
-	svc := things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, up)
+	svc := things.New(auth, thingsRepo, channelsRepo, groupsRepo, chanCache, thingCache, up)
 	svc = rediscache.NewEventStoreMiddleware(svc, esClient)
 	svc = api.LoggingMiddleware(svc, logger)
 	svc = api.MetricsMiddleware(
diff --git a/internal/groups/api/endpoint.go b/internal/groups/api/endpoint.go
new file mode 100644
index 00000000..348346cd
--- /dev/null
+++ b/internal/groups/api/endpoint.go
@@ -0,0 +1,337 @@
+package groups
+
+import (
+	"context"
+
+	"github.com/go-kit/kit/endpoint"
+	"github.com/mainflux/mainflux/internal/groups"
+	"github.com/mainflux/mainflux/pkg/errors"
+)
+
+func CreateGroupEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(createGroupReq)
+		if err := req.validate(); err != nil {
+			return groupRes{}, err
+		}
+
+		group := groups.Group{
+			Name:        req.Name,
+			Description: req.Description,
+			ParentID:    req.ParentID,
+			Metadata:    req.Metadata,
+		}
+
+		id, err := svc.CreateGroup(ctx, req.token, group)
+		if err != nil {
+			return groupRes{}, errors.Wrap(groups.ErrCreateGroup, err)
+		}
+
+		return groupRes{created: true, id: id}, nil
+	}
+}
+
+func ViewGroupEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(groupReq)
+		if err := req.validate(); err != nil {
+			return viewGroupRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		group, err := svc.ViewGroup(ctx, req.token, req.groupID)
+		if err != nil {
+			return viewGroupRes{}, errors.Wrap(groups.ErrFetchGroups, err)
+		}
+
+		res := viewGroupRes{
+			ID:          group.ID,
+			Name:        group.Name,
+			Description: group.Description,
+			Metadata:    group.Metadata,
+			ParentID:    group.ParentID,
+			OwnerID:     group.OwnerID,
+		}
+
+		return res, nil
+	}
+}
+
+func UpdateGroupEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(updateGroupReq)
+		if err := req.validate(); err != nil {
+			return groupRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		group := groups.Group{
+			ID:          req.id,
+			Name:        req.Name,
+			Description: req.Description,
+			ParentID:    req.ParentID,
+			Metadata:    req.Metadata,
+		}
+
+		_, err := svc.UpdateGroup(ctx, req.token, group)
+		if err != nil {
+			return groupRes{}, errors.Wrap(groups.ErrUpdateGroup, err)
+		}
+
+		res := groupRes{created: false}
+		return res, nil
+	}
+}
+
+func DeleteGroupEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(groupReq)
+		if err := req.validate(); err != nil {
+			return nil, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		if err := svc.RemoveGroup(ctx, req.token, req.groupID); err != nil {
+			return nil, errors.Wrap(groups.ErrDeleteGroup, err)
+		}
+
+		return groupDeleteRes{}, nil
+	}
+}
+
+func ListGroupsEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(listGroupsReq)
+		if err := req.validate(); err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		page, err := svc.ListGroups(ctx, req.token, req.level, req.metadata)
+		if err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
+		}
+
+		if req.tree {
+			return buildGroupsResponseTree(page), nil
+		}
+
+		return buildGroupsResponse(page), nil
+	}
+}
+
+func ListMembership(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(listMemberGroupReq)
+		if err := req.validate(); err != nil {
+			return memberPageRes{}, err
+		}
+
+		page, err := svc.ListMemberships(ctx, req.token, req.memberID, req.offset, req.limit, req.metadata)
+		if err != nil {
+			return memberPageRes{}, err
+		}
+
+		if req.tree {
+			return buildGroupsResponseTree(page), nil
+		}
+
+		return buildGroupsResponse(page), nil
+	}
+}
+
+func ListGroupChildrenEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(listGroupsReq)
+		if err := req.validate(); err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		page, err := svc.ListChildren(ctx, req.token, req.groupID, req.level, req.metadata)
+		if err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
+		}
+
+		if req.tree {
+			return buildGroupsResponseTree(page), nil
+		}
+
+		return buildGroupsResponse(page), nil
+	}
+}
+
+func ListGroupParentsEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(listGroupsReq)
+		if err := req.validate(); err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		page, err := svc.ListParents(ctx, req.token, req.groupID, req.level, req.metadata)
+		if err != nil {
+			return groupPageRes{}, errors.Wrap(groups.ErrFetchGroups, err)
+		}
+
+		if req.tree {
+			return buildGroupsResponseTree(page), nil
+		}
+
+		return buildGroupsResponse(page), nil
+	}
+}
+
+func AssignEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(memberGroupReq)
+		if err := req.validate(); err != nil {
+			return nil, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		if err := svc.Assign(ctx, req.token, req.memberID, req.groupID); err != nil {
+			return nil, errors.Wrap(groups.ErrAssignToGroup, err)
+		}
+
+		return assignMemberToGroupRes{}, nil
+	}
+}
+
+func UnassignEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(memberGroupReq)
+		if err := req.validate(); err != nil {
+			return nil, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		if err := svc.Unassign(ctx, req.token, req.memberID, req.groupID); err != nil {
+			return nil, errors.Wrap(groups.ErrUnassignFromGroup, err)
+		}
+
+		return removeMemberFromGroupRes{}, nil
+	}
+}
+
+func ListMembersEndpoint(svc groups.Service) endpoint.Endpoint {
+	return func(ctx context.Context, request interface{}) (interface{}, error) {
+		req := request.(listMemberGroupReq)
+		if err := req.validate(); err != nil {
+			return memberPageRes{}, errors.Wrap(groups.ErrMalformedEntity, err)
+		}
+
+		page, err := svc.ListMembers(ctx, req.token, req.groupID, req.offset, req.limit, req.metadata)
+		if err != nil {
+			return memberPageRes{}, err
+		}
+
+		return buildUsersResponse(page), nil
+	}
+}
+
+func buildGroupsResponseTree(page groups.GroupPage) groupPageRes {
+	groupsMap := map[string]*groups.Group{}
+	// Parents map keeps its array of children.
+	parentsMap := map[string][]*groups.Group{}
+	for i := range page.Groups {
+		if _, ok := groupsMap[page.Groups[i].ID]; !ok {
+			groupsMap[page.Groups[i].ID] = &page.Groups[i]
+			parentsMap[page.Groups[i].ID] = make([]*groups.Group, 0)
+		}
+	}
+
+	for _, group := range groupsMap {
+		if ch, ok := parentsMap[group.ParentID]; ok {
+			ch = append(ch, group)
+			parentsMap[group.ParentID] = ch
+		}
+	}
+
+	res := groupPageRes{
+		pageRes: pageRes{
+			Total:  page.Total,
+			Offset: page.Offset,
+			Limit:  page.Limit,
+		},
+		Groups: []viewGroupRes{},
+	}
+
+	for _, group := range groupsMap {
+		if children, ok := parentsMap[group.ID]; ok {
+			group.Children = children
+		}
+
+	}
+
+	for _, group := range groupsMap {
+		view := toViewGroupRes(*group)
+		if children, ok := parentsMap[group.ParentID]; len(children) == 0 || !ok {
+			res.Groups = append(res.Groups, view)
+		}
+	}
+
+	return res
+}
+
+func toViewGroupRes(g groups.Group) viewGroupRes {
+	view := viewGroupRes{
+		ID:          g.ID,
+		ParentID:    g.ParentID,
+		OwnerID:     g.OwnerID,
+		Name:        g.Name,
+		Description: g.Description,
+		Metadata:    g.Metadata,
+		Level:       g.Level,
+		Path:        g.Path,
+		Children:    make([]*viewGroupRes, 0),
+		CreatedAt:   g.CreatedAt,
+		UpdatedAt:   g.UpdatedAt,
+	}
+
+	for _, ch := range g.Children {
+		child := toViewGroupRes(*ch)
+		view.Children = append(view.Children, &child)
+	}
+
+	return view
+}
+
+func buildGroupsResponse(gp groups.GroupPage) groupPageRes {
+	res := groupPageRes{
+		pageRes: pageRes{
+			Total:  gp.Total,
+			Offset: gp.Offset,
+			Limit:  gp.Limit,
+		},
+		Groups: []viewGroupRes{},
+	}
+
+	for _, group := range gp.Groups {
+		view := viewGroupRes{
+			ID:          group.ID,
+			ParentID:    group.ParentID,
+			OwnerID:     group.OwnerID,
+			Name:        group.Name,
+			Description: group.Description,
+			Metadata:    group.Metadata,
+			Level:       group.Level,
+			Path:        group.Path,
+			CreatedAt:   group.CreatedAt,
+			UpdatedAt:   group.UpdatedAt,
+		}
+		res.Groups = append(res.Groups, view)
+	}
+
+	return res
+}
+
+func buildUsersResponse(mp groups.MemberPage) memberPageRes {
+	res := memberPageRes{
+		pageRes: pageRes{
+			Total:  mp.Total,
+			Offset: mp.Offset,
+			Limit:  mp.Limit,
+			Name:   mp.Name,
+		},
+		Members: []interface{}{},
+	}
+
+	for _, m := range mp.Members {
+		res.Members = append(res.Members, m)
+	}
+
+	return res
+}
diff --git a/internal/groups/api/requests.go b/internal/groups/api/requests.go
new file mode 100644
index 00000000..58aca41e
--- /dev/null
+++ b/internal/groups/api/requests.go
@@ -0,0 +1,138 @@
+package groups
+
+import (
+	"regexp"
+
+	"github.com/mainflux/mainflux/internal/groups"
+	"github.com/mainflux/mainflux/pkg/errors"
+)
+
+var groupRegexp = regexp.MustCompile("^[A-Za-z0-9]+[A-Za-z0-9_-]*$")
+
+type createGroupReq struct {
+	token       string
+	Name        string                 `json:"name,omitempty"`
+	ParentID    string                 `json:"parent_id,omitempty"`
+	Description string                 `json:"description,omitempty"`
+	Metadata    map[string]interface{} `json:"metadata,omitempty"`
+}
+
+func (req createGroupReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+	if len(req.Name) > maxNameSize || req.Name == "" || !groupRegexp.MatchString(req.Name) {
+		return errors.Wrap(groups.ErrMalformedEntity, groups.ErrBadGroupName)
+	}
+
+	return nil
+}
+
+type updateGroupReq struct {
+	token       string
+	id          string
+	Name        string                 `json:"name,omitempty"`
+	Description string                 `json:"description,omitempty"`
+	ParentID    string                 `json:"parent_id,omitempty"`
+	Metadata    map[string]interface{} `json:"metadata,omitempty"`
+}
+
+func (req updateGroupReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+
+	if req.id == "" {
+		return groups.ErrMalformedEntity
+	}
+
+	if req.ParentID != "" {
+		return groups.ErrParentInvariant
+	}
+
+	if req.Name != "" {
+		return groups.ErrNameInvariant
+	}
+
+	return nil
+}
+
+type listGroupsReq struct {
+	token    string
+	level    uint64
+	metadata groups.Metadata
+	name     string
+	groupID  string
+	tree     bool
+}
+
+func (req listGroupsReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+
+	if req.level < 0 || req.level > 5 {
+		return groups.ErrMalformedEntity
+	}
+
+	return nil
+}
+
+type listMemberGroupReq struct {
+	token    string
+	offset   uint64
+	limit    uint64
+	metadata groups.Metadata
+	name     string
+	groupID  string
+	memberID string
+	tree     bool
+}
+
+func (req listMemberGroupReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+
+	if req.groupID == "" && req.memberID == "" {
+		return groups.ErrMalformedEntity
+	}
+
+	return nil
+}
+
+type memberGroupReq struct {
+	token    string
+	groupID  string
+	memberID string
+}
+
+func (req memberGroupReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+
+	if req.groupID == "" && req.memberID == "" {
+		return groups.ErrMalformedEntity
+	}
+
+	return nil
+}
+
+type groupReq struct {
+	token   string
+	groupID string
+	name    string
+}
+
+func (req groupReq) validate() error {
+	if req.token == "" {
+		return groups.ErrUnauthorizedAccess
+	}
+
+	if req.groupID == "" && req.name == "" {
+		return groups.ErrMalformedEntity
+	}
+
+	return nil
+}
diff --git a/internal/groups/api/responses.go b/internal/groups/api/responses.go
new file mode 100644
index 00000000..3fa69f97
--- /dev/null
+++ b/internal/groups/api/responses.go
@@ -0,0 +1,156 @@
+package groups
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/mainflux/mainflux"
+)
+
+var (
+	_ mainflux.Response = (*memberPageRes)(nil)
+	_ mainflux.Response = (*groupRes)(nil)
+	_ mainflux.Response = (*groupDeleteRes)(nil)
+	_ mainflux.Response = (*assignMemberToGroupRes)(nil)
+	_ mainflux.Response = (*removeMemberFromGroupRes)(nil)
+)
+
+type pageRes struct {
+	Total  uint64 `json:"total"`
+	Offset uint64 `json:"offset"`
+	Limit  uint64 `json:"limit"`
+	Name   string `json:"name"`
+}
+
+type memberPageRes struct {
+	pageRes
+	Members []interface{}
+}
+
+func (res memberPageRes) Code() int {
+	return http.StatusOK
+}
+
+func (res memberPageRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res memberPageRes) Empty() bool {
+	return false
+}
+
+type viewGroupRes struct {
+	ID          string                 `json:"id,omitempty"`
+	Name        string                 `json:"name,omitempty"`
+	ParentID    string                 `json:"parent_id,omitempty"`
+	OwnerID     string                 `json:"owner_id,omitempty"`
+	Description string                 `json:"description,omitempty"`
+	Metadata    map[string]interface{} `json:"metadata,omitempty"`
+	// Indicates a level in tree hierarchy from first group node.
+	Level int `json:"level,omitempty"`
+	// Path is a path in a tree, consisted of group names
+	// parentName.childrenName1.childrenName2 .
+	Path      string          `json:"path"`
+	Children  []*viewGroupRes `json:"children"`
+	CreatedAt time.Time       `json:"created_at"`
+	UpdatedAt time.Time       `json:"updated_at"`
+}
+
+func (res viewGroupRes) Code() int {
+	return http.StatusOK
+}
+
+func (res viewGroupRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res viewGroupRes) Empty() bool {
+	return false
+}
+
+type groupRes struct {
+	id      string
+	created bool
+}
+
+func (res groupRes) Code() int {
+	if res.created {
+		return http.StatusCreated
+	}
+
+	return http.StatusOK
+}
+
+func (res groupRes) Headers() map[string]string {
+	if res.created {
+		return map[string]string{
+			"Location": fmt.Sprintf("/groups/%s", res.id),
+		}
+	}
+
+	return map[string]string{}
+}
+
+func (res groupRes) Empty() bool {
+	return true
+}
+
+type groupPageRes struct {
+	pageRes
+	Groups []viewGroupRes `json:"groups"`
+}
+
+func (res groupPageRes) Code() int {
+	return http.StatusOK
+}
+
+func (res groupPageRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res groupPageRes) Empty() bool {
+	return false
+}
+
+type groupDeleteRes struct{}
+
+func (res groupDeleteRes) Code() int {
+	return http.StatusNoContent
+}
+
+func (res groupDeleteRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res groupDeleteRes) Empty() bool {
+	return true
+}
+
+type assignMemberToGroupRes struct{}
+
+func (res assignMemberToGroupRes) Code() int {
+	return http.StatusNoContent
+}
+
+func (res assignMemberToGroupRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res assignMemberToGroupRes) Empty() bool {
+	return true
+}
+
+type removeMemberFromGroupRes struct{}
+
+func (res removeMemberFromGroupRes) Code() int {
+	return http.StatusNoContent
+}
+
+func (res removeMemberFromGroupRes) Headers() map[string]string {
+	return map[string]string{}
+}
+
+func (res removeMemberFromGroupRes) Empty() bool {
+	return true
+}
diff --git a/internal/groups/api/transport.go b/internal/groups/api/transport.go
new file mode 100644
index 00000000..963a5c9e
--- /dev/null
+++ b/internal/groups/api/transport.go
@@ -0,0 +1,227 @@
+package groups
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/go-zoo/bone"
+	"github.com/mainflux/mainflux/internal/groups"
+	"github.com/mainflux/mainflux/pkg/errors"
+)
+
+var errInvalidQueryParams = errors.New("invalid query params")
+
+const (
+	maxNameSize = 254
+	offsetKey   = "offset"
+	limitKey    = "limit"
+	nameKey     = "name"
+	levelKey    = "level"
+	metadataKey = "metadata"
+	treeKey     = "tree"
+	contentType = "application/json"
+
+	defOffset = 0
+	defLimit  = 10
+	defLevel  = 1
+)
+
+func DecodeListGroupsRequest(_ context.Context, r *http.Request) (interface{}, error) {
+	if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
+		return nil, groups.ErrUnsupportedContentType
+	}
+
+	l, err := readUintQuery(r, levelKey, defLevel)
+	if err != nil {
+		return nil, err
+	}
+
+	n, err := readStringQuery(r, nameKey)
+	if err != nil {
+		return nil, err
+	}
+
+	m, err := readMetadataQuery(r, metadataKey)
+	if err != nil {
+		return nil, err
+	}
+
+	t, err := readBoolQuery(r, treeKey)
+	if err != nil {
+		return nil, err
+	}
+
+	req := listGroupsReq{
+		token:    r.Header.Get("Authorization"),
+		level:    l,
+		name:     n,
+		metadata: m,
+		tree:     t,
+		groupID:  bone.GetValue(r, "groupID"),
+	}
+	return req, nil
+}
+
+func DecodeListMemberGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
+	if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
+		return nil, groups.ErrUnsupportedContentType
+	}
+
+	o, err := readUintQuery(r, offsetKey, defOffset)
+	if err != nil {
+		return nil, err
+	}
+
+	l, err := readUintQuery(r, limitKey, defLimit)
+	if err != nil {
+		return nil, err
+	}
+
+	n, err := readStringQuery(r, nameKey)
+	if err != nil {
+		return nil, err
+	}
+
+	m, err := readMetadataQuery(r, metadataKey)
+	if err != nil {
+		return nil, err
+	}
+
+	t, err := readBoolQuery(r, treeKey)
+	if err != nil {
+		return nil, err
+	}
+
+	req := listMemberGroupReq{
+		token:    r.Header.Get("Authorization"),
+		groupID:  bone.GetValue(r, "groupID"),
+		memberID: bone.GetValue(r, "memberID"),
+		offset:   o,
+		limit:    l,
+		name:     n,
+		metadata: m,
+		tree:     t,
+	}
+	return req, nil
+}
+
+func DecodeGroupCreate(_ context.Context, r *http.Request) (interface{}, error) {
+	if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
+		return nil, groups.ErrUnsupportedContentType
+	}
+
+	var req createGroupReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return nil, errors.Wrap(groups.ErrFailedDecode, err)
+	}
+
+	req.token = r.Header.Get("Authorization")
+	return req, nil
+}
+
+func DecodeGroupUpdate(_ context.Context, r *http.Request) (interface{}, error) {
+	if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
+		return nil, groups.ErrUnsupportedContentType
+	}
+
+	var req updateGroupReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		return nil, errors.Wrap(groups.ErrFailedDecode, err)
+	}
+
+	req.id = bone.GetValue(r, "groupID")
+	req.token = r.Header.Get("Authorization")
+	return req, nil
+}
+
+func DecodeGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
+	req := groupReq{
+		token:   r.Header.Get("Authorization"),
+		groupID: bone.GetValue(r, "groupID"),
+		name:    bone.GetValue(r, "name"),
+	}
+
+	return req, nil
+}
+
+func DecodeMemberGroupRequest(_ context.Context, r *http.Request) (interface{}, error) {
+	req := memberGroupReq{
+		token:    r.Header.Get("Authorization"),
+		groupID:  bone.GetValue(r, "groupID"),
+		memberID: bone.GetValue(r, "memberID"),
+	}
+
+	return req, nil
+}
+
+func readUintQuery(r *http.Request, key string, def uint64) (uint64, error) {
+	vals := bone.GetQuery(r, key)
+	if len(vals) > 1 {
+		return 0, errInvalidQueryParams
+	}
+
+	if len(vals) == 0 {
+		return def, nil
+	}
+
+	strval := vals[0]
+	val, err := strconv.ParseUint(strval, 10, 64)
+	if err != nil {
+		return 0, errInvalidQueryParams
+	}
+
+	return val, nil
+}
+
+func readStringQuery(r *http.Request, key string) (string, error) {
+	vals := bone.GetQuery(r, key)
+	if len(vals) > 1 {
+		return "", errInvalidQueryParams
+	}
+
+	if len(vals) == 0 {
+		return "", nil
+	}
+
+	return vals[0], nil
+}
+
+func readMetadataQuery(r *http.Request, key string) (map[string]interface{}, error) {
+	vals := bone.GetQuery(r, key)
+	if len(vals) > 1 {
+		return nil, errInvalidQueryParams
+	}
+
+	if len(vals) == 0 {
+		return nil, nil
+	}
+
+	m := make(map[string]interface{})
+	err := json.Unmarshal([]byte(vals[0]), &m)
+	if err != nil {
+		return nil, errors.Wrap(errInvalidQueryParams, err)
+	}
+
+	return m, nil
+}
+
+func readBoolQuery(r *http.Request, key string) (bool, error) {
+	vals := bone.GetQuery(r, key)
+	if len(vals) > 1 {
+		return true, errInvalidQueryParams
+	}
+
+	if len(vals) == 0 {
+		return false, nil
+	}
+
+	b, err := strconv.ParseBool(vals[0])
+	if err != nil {
+		return false, errInvalidQueryParams
+	}
+
+	return b, nil
+}
diff --git a/internal/groups/errors.go b/internal/groups/errors.go
new file mode 100644
index 00000000..739ed4f7
--- /dev/null
+++ b/internal/groups/errors.go
@@ -0,0 +1,53 @@
+package groups
+
+import "github.com/mainflux/mainflux/pkg/errors"
+
+var (
+	// ErrUnauthorizedAccess unauthorized access.
+	ErrUnauthorizedAccess = errors.New("unauthorized access")
+
+	// ErrMalformedEntity malformed entity.
+	ErrMalformedEntity = errors.New("malformed entity")
+
+	// ErrBadGroupName malformed entity.
+	ErrBadGroupName = errors.New("incorrect group name")
+
+	// ErrGroupConflict group conflict.
+	ErrGroupConflict = errors.New("group already exists")
+
+	// ErrCreateGroup indicates failure to create group.
+	ErrCreateGroup = errors.New("failed to create group")
+
+	// ErrFetchGroups indicates failure to fetch groups.
+	ErrFetchGroups = errors.New("failed to fetch groups")
+
+	// ErrUpdateGroup indicates failure to update group.
+	ErrUpdateGroup = errors.New("failed to update group")
+
+	// ErrDeleteGroup indicates failure to delete group.
+	ErrDeleteGroup = errors.New("failed to delete group")
+
+	// ErrNotFound indicates failure to find group.
+	ErrNotFound = errors.New("failed to find group")
+
+	// ErrAssignToGroup indicates failure to assign member to a group.
+	ErrAssignToGroup = errors.New("failed to assign member to a group")
+
+	// ErrUnassignFromGroup indicates failure to unassign member from a group.
+	ErrUnassignFromGroup = errors.New("failed to unassign member from a group")
+
+	// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type
+	ErrUnsupportedContentType = errors.New("unsupported content type")
+
+	// ErrFailedDecode indicates failed to decode request body
+	ErrFailedDecode = errors.New("failed to decode request body")
+
+	// ErrMissingParent indicates that parent can't be found
+	ErrMissingParent = errors.New("failed to retrieve parent")
+
+	// ErrNameInvariant indicates that name can't be changed
+	ErrNameInvariant = errors.New("name can't be changed")
+
+	// ErrParentInvariant indicates that parent can't be changed
+	ErrParentInvariant = errors.New("parent can't be changed")
+)
diff --git a/internal/groups/groups.go b/internal/groups/groups.go
new file mode 100644
index 00000000..9d247782
--- /dev/null
+++ b/internal/groups/groups.go
@@ -0,0 +1,114 @@
+package groups
+
+import (
+	"context"
+	"time"
+)
+
+type Member interface{}
+type Metadata map[string]interface{}
+
+type Group struct {
+	ID          string
+	OwnerID     string
+	ParentID    string
+	Name        string
+	Description string
+	Metadata    Metadata
+	// Indicates a level in hierarchy from first group node.
+	// For a root node level is 1.
+	Level int
+	// Path is a path in a tree, consisted of group names
+	// parentName.childrenName1.childrenName2 .
+	Path      string
+	Children  []*Group
+	CreatedAt time.Time
+	UpdatedAt  time.Time
+}
+
+type PageMetadata struct {
+	Total  uint64
+	Offset uint64
+	Limit  uint64
+	Name   string
+}
+
+type GroupPage struct {
+	PageMetadata
+	Groups []Group
+}
+
+type MemberPage struct {
+	PageMetadata
+	Members []Member
+}
+
+type Service interface {
+	// CreateGroup creates new  group.
+	CreateGroup(ctx context.Context, token string, g Group) (string, error)
+
+	// UpdateGroup updates the group identified by the provided ID.
+	UpdateGroup(ctx context.Context, token string, g Group) (Group, error)
+
+	// ViewGroup retrieves data about the group identified by ID.
+	ViewGroup(ctx context.Context, token, id string) (Group, error)
+
+	// ListGroups retrieves groups.
+	ListGroups(ctx context.Context, token string, level uint64, m Metadata) (GroupPage, error)
+
+	// ListChildren retrieves groups that are children to group identified by parentID
+	ListChildren(ctx context.Context, token, parentID string, level uint64, m Metadata) (GroupPage, error)
+
+	// ListParents retrieves groups that are parent to group identified by childID.
+	ListParents(ctx context.Context, token, childID string, level uint64, m Metadata) (GroupPage, error)
+
+	// ListMembers retrieves everything that is assigned to a group identified by groupID.
+	ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, m Metadata) (MemberPage, error)
+
+	// ListMemberships retrieves all groups for member that is identified with memberID belongs to.
+	ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, m Metadata) (GroupPage, error)
+
+	// RemoveGroup removes the group identified with the provided ID.
+	RemoveGroup(ctx context.Context, token, id string) error
+
+	// Assign adds  member with memberID into the group identified by groupID.
+	Assign(ctx context.Context, token, memberID, groupID string) error
+
+	// Unassign removes member with memberID from group identified by groupID.
+	Unassign(ctx context.Context, token, memberID, groupID string) error
+}
+
+type Repository interface {
+	// Save group
+	Save(ctx context.Context, g Group) (Group, error)
+
+	// Update a group
+	Update(ctx context.Context, g Group) (Group, error)
+
+	// Delete a group
+	Delete(ctx context.Context, groupID string) error
+
+	// RetrieveByID retrieves group by its id
+	RetrieveByID(ctx context.Context, id string) (Group, error)
+
+	// RetrieveAll retrieves all groups.
+	RetrieveAll(ctx context.Context, level uint64, m Metadata) (GroupPage, error)
+
+	// RetrieveAllParents retrieves all groups that are ancestors to the group with given groupID.
+	RetrieveAllParents(ctx context.Context, groupID string, level uint64, m Metadata) (GroupPage, error)
+
+	// RetrieveAllChildren retrieves all children from group with given groupID up to the hierarchy level.
+	RetrieveAllChildren(ctx context.Context, groupID string, level uint64, m Metadata) (GroupPage, error)
+
+	// Retrieves list of groups that member belongs to
+	Memberships(ctx context.Context, memberID string, offset, limit uint64, m Metadata) (GroupPage, error)
+
+	// Members retrieves everything that is assigned to a group identified by groupID.
+	Members(ctx context.Context, groupID string, offset, limit uint64, m Metadata) (MemberPage, error)
+
+	// Assign adds member to group.
+	Assign(ctx context.Context, memberID, groupID string) error
+
+	// Unassign removes a member from a group
+	Unassign(ctx context.Context, memberID, groupID string) error
+}
diff --git a/things/api/logging.go b/things/api/logging.go
index bbfcb4cd..5a31fc4c 100644
--- a/things/api/logging.go
+++ b/things/api/logging.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/mainflux/mainflux/internal/groups"
 	log "github.com/mainflux/mainflux/logger"
 	"github.com/mainflux/mainflux/things"
 )
@@ -165,7 +166,7 @@ func (lm *loggingMiddleware) ListChannels(ctx context.Context, token string, off
 		if name != "" {
 			nlog = fmt.Sprintf("with name %s ", name)
 		}
-		message := fmt.Sprintf("Method list_channels %sfor token %s took %s to complete", nlog, token, time.Since(begin))
+		message := fmt.Sprintf("Method list_channels %s for token %s took %s to complete", nlog, token, time.Since(begin))
 		if err != nil {
 			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
 			return
@@ -254,7 +255,7 @@ func (lm *loggingMiddleware) CanAccessByID(ctx context.Context, chanID, thingID
 }
 func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id string, err error) {
 	defer func(begin time.Time) {
-		message := fmt.Sprintf("Method identify for key %s and thing %s took %s to complete", key, id, time.Since(begin))
+		message := fmt.Sprintf("Method identify for token %s and thing %s took %s to complete", key, id, time.Since(begin))
 		if err != nil {
 			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
 			return
@@ -264,3 +265,146 @@ func (lm *loggingMiddleware) Identify(ctx context.Context, key string) (id strin
 
 	return lm.svc.Identify(ctx, key)
 }
+
+func (lm *loggingMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method create_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.CreateGroup(ctx, token, g)
+}
+
+func (lm *loggingMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method update_group for token %s and name %s took %s to complete", token, g.Name, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.UpdateGroup(ctx, token, g)
+}
+
+func (lm *loggingMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method remove_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.RemoveGroup(ctx, token, id)
+}
+
+func (lm *loggingMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method view_group for token %s and id %s took %s to complete", token, id, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ViewGroup(ctx, token, id)
+}
+
+func (lm *loggingMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method list_groups for token %s took %s to complete", token, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ListGroups(ctx, token, level, gm)
+}
+
+func (lm *loggingMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method list_children for token %s and parent %s took %s to complete", token, parentID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ListChildren(ctx, token, parentID, level, gm)
+}
+
+func (lm *loggingMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method list_parents for token %s and child %s took for child %s to complete", token, childID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ListParents(ctx, token, childID, level, gm)
+}
+
+func (lm *loggingMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method list_members for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
+}
+
+func (lm *loggingMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method list_memberships for token %s and group id %s took %s to complete", token, groupID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
+}
+
+func (lm *loggingMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method assign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.Assign(ctx, token, memberID, groupID)
+}
+
+func (lm *loggingMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
+	defer func(begin time.Time) {
+		message := fmt.Sprintf("Method unassign for token %s and member %s group id %s took %s to complete", token, memberID, groupID, time.Since(begin))
+		if err != nil {
+			lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
+			return
+		}
+		lm.logger.Info(fmt.Sprintf("%s without errors.", message))
+	}(time.Now())
+
+	return lm.svc.Unassign(ctx, token, memberID, groupID)
+}
diff --git a/things/api/metrics.go b/things/api/metrics.go
index d29fcf76..a8b32eda 100644
--- a/things/api/metrics.go
+++ b/things/api/metrics.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	"github.com/go-kit/kit/metrics"
+	"github.com/mainflux/mainflux/internal/groups"
 	"github.com/mainflux/mainflux/things"
 )
 
@@ -192,3 +193,99 @@ func (ms *metricsMiddleware) Identify(ctx context.Context, key string) (string,
 
 	return ms.svc.Identify(ctx, key)
 }
+
+func (ms *metricsMiddleware) CreateGroup(ctx context.Context, token string, g groups.Group) (id string, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "create_group").Add(1)
+		ms.latency.With("method", "create_group").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+	return ms.svc.CreateGroup(ctx, token, g)
+}
+
+func (ms *metricsMiddleware) UpdateGroup(ctx context.Context, token string, g groups.Group) (gr groups.Group, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "update_group").Add(1)
+		ms.latency.With("method", "update_group").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+	return ms.svc.UpdateGroup(ctx, token, g)
+}
+
+func (ms *metricsMiddleware) RemoveGroup(ctx context.Context, token string, id string) (err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "remove_group").Add(1)
+		ms.latency.With("method", "remove_group").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+	return ms.svc.RemoveGroup(ctx, token, id)
+}
+
+func (ms *metricsMiddleware) ViewGroup(ctx context.Context, token, id string) (g groups.Group, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "view_group").Add(1)
+		ms.latency.With("method", "view_group").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ViewGroup(ctx, token, id)
+}
+
+func (ms *metricsMiddleware) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "list_groups").Add(1)
+		ms.latency.With("method", "list_groups").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ListGroups(ctx, token, level, gm)
+}
+
+func (ms *metricsMiddleware) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "parents").Add(1)
+		ms.latency.With("method", "parents").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ListParents(ctx, token, childID, level, gm)
+}
+
+func (ms *metricsMiddleware) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "list_children").Add(1)
+		ms.latency.With("method", "list_children").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ListChildren(ctx, token, parentID, level, gm)
+}
+
+func (ms *metricsMiddleware) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.MemberPage, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "list_members").Add(1)
+		ms.latency.With("method", "list_members").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
+}
+
+func (ms *metricsMiddleware) ListMemberships(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (gp groups.GroupPage, err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "list_memberships").Add(1)
+		ms.latency.With("method", "list_memberships").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.ListMemberships(ctx, token, groupID, offset, limit, gm)
+}
+
+func (ms *metricsMiddleware) Assign(ctx context.Context, token, memberID, groupID string) (err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "assign").Add(1)
+		ms.latency.With("method", "assign").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.Assign(ctx, token, memberID, groupID)
+}
+
+func (ms *metricsMiddleware) Unassign(ctx context.Context, token, memberID, groupID string) (err error) {
+	defer func(begin time.Time) {
+		ms.counter.With("method", "unassign").Add(1)
+		ms.latency.With("method", "unassign").Observe(time.Since(begin).Seconds())
+	}(time.Now())
+
+	return ms.svc.Unassign(ctx, token, memberID, groupID)
+}
diff --git a/things/api/things/http/transport.go b/things/api/things/http/transport.go
index 0229ed3b..481c17fe 100644
--- a/things/api/things/http/transport.go
+++ b/things/api/things/http/transport.go
@@ -15,6 +15,8 @@ import (
 	kithttp "github.com/go-kit/kit/transport/http"
 	"github.com/go-zoo/bone"
 	"github.com/mainflux/mainflux"
+	"github.com/mainflux/mainflux/internal/groups"
+	groupsAPI "github.com/mainflux/mainflux/internal/groups/api"
 	"github.com/mainflux/mainflux/pkg/errors"
 	"github.com/mainflux/mainflux/things"
 	opentracing "github.com/opentracing/opentracing-go"
@@ -172,6 +174,83 @@ func MakeHandler(tracer opentracing.Tracer, svc things.Service) http.Handler {
 		opts...,
 	))
 
+	r.Get("/things/:memberID/groups", kithttp.NewServer(
+		kitot.TraceServer(tracer, "list_memberships")(groupsAPI.ListMembership(svc)),
+		groupsAPI.DecodeListMemberGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Post("/groups", kithttp.NewServer(
+		kitot.TraceServer(tracer, "add_group")(groupsAPI.CreateGroupEndpoint(svc)),
+		groupsAPI.DecodeGroupCreate,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Get("/groups", kithttp.NewServer(
+		kitot.TraceServer(tracer, "list_groups")(groupsAPI.ListGroupsEndpoint(svc)),
+		groupsAPI.DecodeListGroupsRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Delete("/groups/:groupID", kithttp.NewServer(
+		kitot.TraceServer(tracer, "delete_group")(groupsAPI.DeleteGroupEndpoint(svc)),
+		groupsAPI.DecodeGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Put("/groups/:groupID/things/:memberID", kithttp.NewServer(
+		kitot.TraceServer(tracer, "assign")(groupsAPI.AssignEndpoint(svc)),
+		groupsAPI.DecodeMemberGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Delete("/groups/:groupID/things/:memberID", kithttp.NewServer(
+		kitot.TraceServer(tracer, "unassign")(groupsAPI.UnassignEndpoint(svc)),
+		groupsAPI.DecodeMemberGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Get("/groups/:groupID/things", kithttp.NewServer(
+		kitot.TraceServer(tracer, "list_things")(groupsAPI.ListMembersEndpoint(svc)),
+		groupsAPI.DecodeListMemberGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Put("/groups/:groupID", kithttp.NewServer(
+		kitot.TraceServer(tracer, "update_group")(groupsAPI.UpdateGroupEndpoint(svc)),
+		groupsAPI.DecodeGroupUpdate,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Get("/groups/:groupID/children", kithttp.NewServer(
+		kitot.TraceServer(tracer, "list_children_groups")(groupsAPI.ListGroupChildrenEndpoint(svc)),
+		groupsAPI.DecodeListGroupsRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Get("/groups/:groupID/parents", kithttp.NewServer(
+		kitot.TraceServer(tracer, "list_parent_groups")(groupsAPI.ListGroupParentsEndpoint(svc)),
+		groupsAPI.DecodeListGroupsRequest,
+		encodeResponse,
+		opts...,
+	))
+
+	r.Get("/groups/:groupID", kithttp.NewServer(
+		kitot.TraceServer(tracer, "view_group")(groupsAPI.ViewGroupEndpoint(svc)),
+		groupsAPI.DecodeGroupRequest,
+		encodeResponse,
+		opts...,
+	))
+
 	r.GetFunc("/version", mainflux.Version("things"))
 	r.Handle("/metrics", promhttp.Handler())
 
@@ -418,7 +497,8 @@ func encodeError(_ context.Context, err error, w http.ResponseWriter) {
 			errors.Contains(errorVal, things.ErrViewEntity),
 			errors.Contains(errorVal, things.ErrRemoveEntity),
 			errors.Contains(errorVal, things.ErrConnect),
-			errors.Contains(errorVal, things.ErrDisconnect):
+			errors.Contains(errorVal, things.ErrDisconnect),
+			errors.Contains(errorVal, groups.ErrCreateGroup):
 			w.WriteHeader(http.StatusBadRequest)
 
 		case errors.Contains(errorVal, io.ErrUnexpectedEOF),
diff --git a/things/postgres/channels.go b/things/postgres/channels.go
index 0fc170ba..29183258 100644
--- a/things/postgres/channels.go
+++ b/things/postgres/channels.go
@@ -453,18 +453,17 @@ func getMetadataQuery(m things.Metadata) ([]byte, string, error) {
 	return mb, mq, nil
 }
 
-func total(ctx context.Context, db Database, query string, params map[string]interface{}) (uint64, error) {
+func total(ctx context.Context, db Database, query string, params interface{}) (uint64, error) {
 	rows, err := db.NamedQueryContext(ctx, query, params)
 	if err != nil {
 		return 0, err
 	}
-
+	defer rows.Close()
 	total := uint64(0)
 	if rows.Next() {
 		if err := rows.Scan(&total); err != nil {
 			return 0, err
 		}
 	}
-
 	return total, nil
 }
diff --git a/things/postgres/groups.go b/things/postgres/groups.go
new file mode 100644
index 00000000..dc79e598
--- /dev/null
+++ b/things/postgres/groups.go
@@ -0,0 +1,628 @@
+// Copyright (c) Mainflux
+// SPDX-License-Identifier: Apache-2.0
+
+package postgres
+
+import (
+	"context"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/gofrs/uuid"
+	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
+	"github.com/mainflux/mainflux/internal/groups"
+	"github.com/mainflux/mainflux/pkg/errors"
+)
+
+const maxLevel = 5
+
+var (
+	errDeleteGroupDB          = errors.New("delete group failed")
+	errSelectDb               = errors.New("select group from db error")
+	errConvertingStringToUUID = errors.New("error converting string")
+)
+
+var _ groups.Repository = (*groupRepository)(nil)
+
+type groupRepository struct {
+	db Database
+}
+
+// NewGroupRepo instantiates a PostgreSQL implementation of group
+// repository.
+func NewGroupRepo(db Database) groups.Repository {
+	return &groupRepository{
+		db: db,
+	}
+}
+
+func (gr groupRepository) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
+	var id string
+	q := `INSERT INTO thing_groups (name, description, id, owner_id, metadata, path, created_at, updated_at) 
+		  VALUES (:name, :description, :id, :owner_id, :metadata, :name, now(), now()) RETURNING id`
+	if g.ParentID != "" {
+		q = `INSERT INTO thing_groups (name, description, id, owner_id, parent_id, metadata, path) 
+			 SELECT :name, :description, :id, :owner_id, :parent_id, :metadata, text2ltree(ltree2text(tg.path) || '.' || :name) FROM thing_groups tg WHERE id = :parent_id RETURNING id`
+	}
+
+	dbu, err := toDBGroup(g)
+	if err != nil {
+		return groups.Group{}, err
+	}
+
+	row, err := gr.db.NamedQueryContext(ctx, q, dbu)
+	if err != nil {
+		pqErr, ok := err.(*pq.Error)
+		if ok {
+			switch pqErr.Code.Name() {
+			case errInvalid, errTruncation:
+				return groups.Group{}, errors.Wrap(groups.ErrMalformedEntity, err)
+			case errDuplicate:
+				return groups.Group{}, errors.Wrap(groups.ErrGroupConflict, err)
+			}
+		}
+
+		return groups.Group{}, errors.Wrap(groups.ErrCreateGroup, err)
+	}
+
+	defer row.Close()
+	row.Next()
+	if err := row.Scan(&id); err != nil {
+		return groups.Group{}, err
+	}
+	g.ID = id
+	return g, nil
+}
+
+func (gr groupRepository) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
+	q := `UPDATE thing_groups SET description = :description, metadata = :metadata, updated_at = now()  WHERE id = :id`
+
+	dbu, err := toDBGroup(g)
+	if err != nil {
+		return groups.Group{}, errors.Wrap(errUpdateDB, err)
+	}
+
+	if _, err := gr.db.NamedExecContext(ctx, q, dbu); err != nil {
+		return groups.Group{}, errors.Wrap(errUpdateDB, err)
+	}
+
+	return g, nil
+}
+
+func (gr groupRepository) Delete(ctx context.Context, groupID string) error {
+	qd := `DELETE FROM thing_groups WHERE id = :id`
+	group := groups.Group{
+		ID: groupID,
+	}
+	dbg, err := toDBGroup(group)
+	if err != nil {
+		return errors.Wrap(errUpdateDB, err)
+	}
+
+	res, err := gr.db.NamedExecContext(ctx, qd, dbg)
+	if err != nil {
+		return errors.Wrap(errDeleteGroupDB, err)
+	}
+
+	cnt, err := res.RowsAffected()
+	if err != nil {
+		return errors.Wrap(errDeleteGroupDB, err)
+	}
+
+	if cnt != 1 {
+		return errors.Wrap(groups.ErrDeleteGroup, err)
+	}
+	return nil
+}
+
+func (gr groupRepository) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
+	groupID, err := toUUID(id)
+	if err != nil || !groupID.Valid {
+		return groups.Group{}, err
+	}
+
+	dbu := dbGroup{
+		ID: groupID,
+	}
+
+	q := `SELECT id, name, owner_id, parent_id, description, metadata, path, nlevel(path) as level FROM thing_groups WHERE id = $1`
+	if err := gr.db.QueryRowxContext(ctx, q, id).StructScan(&dbu); err != nil {
+		if err == sql.ErrNoRows {
+			return groups.Group{}, errors.Wrap(groups.ErrNotFound, err)
+
+		}
+		return groups.Group{}, errors.Wrap(errRetrieveDB, err)
+	}
+
+	return toGroup(dbu)
+}
+
+func (gr groupRepository) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
+	}
+	if mq != "" {
+		mq = fmt.Sprintf("AND %s", mq)
+	}
+
+	q := fmt.Sprintf(`SELECT id, owner_id, parent_id, name, description, metadata, path, nlevel(path) as level, created_at, updated_at FROM thing_groups 
+					  WHERE nlevel(path) <= :level %s ORDER BY path`, mq)
+	cq := fmt.Sprintf("SELECT COUNT(*) FROM thing_groups WHERE nlevel(path) <= :level %s", mq)
+
+	dbPage, err := toDBGroupPage("", "", "", "", level, gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+	defer rows.Close()
+
+	items, err := processRows(rows)
+	if err != nil {
+		return groups.GroupPage{}, err
+	}
+
+	total, err := total(ctx, gr.db, cq, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	page := groups.GroupPage{
+		Groups: items,
+		PageMetadata: groups.PageMetadata{
+			Total: total,
+		},
+	}
+
+	return page, nil
+}
+
+func (gr groupRepository) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if groupID == "" {
+		return groups.GroupPage{}, nil
+	}
+
+	_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
+	}
+	if mq != "" {
+		mq = fmt.Sprintf("AND %s", mq)
+	}
+
+	q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at
+					  FROM thing_groups parent, thing_groups g
+					  WHERE parent.id = :parent_id AND g.path @> parent.path AND nlevel(parent.path) - nlevel(g.path) <= :level %s`, mq)
+
+	cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :parent_id AND g.path @> parent.path %s`, mq)
+
+	if level > maxLevel {
+		level = maxLevel
+	}
+
+	dbPage, err := toDBGroupPage("", "", groupID, "", level, gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+	defer rows.Close()
+
+	items, err := processRows(rows)
+	if err != nil {
+		return groups.GroupPage{}, err
+	}
+
+	total, err := total(ctx, gr.db, cq, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	page := groups.GroupPage{
+		Groups: items,
+		PageMetadata: groups.PageMetadata{
+			Total: total,
+		},
+	}
+
+	return page, nil
+}
+
+func (gr groupRepository) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if groupID == "" {
+		return groups.GroupPage{}, nil
+	}
+	_, mq, err := getGroupsMetadataQuery("thing_groups", gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
+	}
+	if mq != "" {
+		mq = fmt.Sprintf("AND %s", mq)
+	}
+
+	q := fmt.Sprintf(`SELECT g.id, g.name, g.owner_id, g.parent_id, g.description, g.metadata, g.path, nlevel(g.path) as level, g.created_at, g.updated_at 
+					  FROM thing_groups parent, thing_groups g
+					  WHERE parent.id = :id AND g.path <@ parent.path AND nlevel(g.path) - nlevel(parent.path) <= :level %s`, mq)
+
+	cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_groups parent, thing_groups g WHERE parent.id = :id AND g.path <@ parent.path %s`, mq)
+
+	if level > maxLevel {
+		level = maxLevel
+	}
+
+	dbPage, err := toDBGroupPage("", groupID, "", "", level, gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	rows, err := gr.db.NamedQueryContext(ctx, q, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+	defer rows.Close()
+
+	items, err := processRows(rows)
+	if err != nil {
+		return groups.GroupPage{}, err
+	}
+
+	total, err := total(ctx, gr.db, cq, dbPage)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	page := groups.GroupPage{
+		Groups: items,
+		PageMetadata: groups.PageMetadata{
+			Total: total,
+		},
+	}
+
+	return page, nil
+}
+
+func (gr groupRepository) Members(ctx context.Context, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
+	m, mq, err := getGroupsMetadataQuery("things_group", gm)
+	if err != nil {
+		return groups.MemberPage{}, errors.Wrap(errRetrieveDB, err)
+	}
+
+	q := fmt.Sprintf(`SELECT th.id, th.name, th.key, th.metadata FROM things th, thing_group_relations g
+					  WHERE th.id = g.thing_id AND g.group_id = :group 
+					  %s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
+
+	params := map[string]interface{}{
+		"group":    groupID,
+		"limit":    limit,
+		"offset":   offset,
+		"metadata": m,
+	}
+
+	rows, err := gr.db.NamedQueryContext(ctx, q, params)
+	if err != nil {
+		return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
+	}
+	defer rows.Close()
+
+	var items []groups.Member
+	for rows.Next() {
+		dbTh := dbThing{}
+		if err := rows.StructScan(&dbTh); err != nil {
+			return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
+		}
+
+		thing, err := toThing(dbTh)
+		if err != nil {
+			return groups.MemberPage{}, err
+		}
+
+		items = append(items, thing)
+	}
+
+	cq := fmt.Sprintf(`SELECT COUNT(*) FROM things th, thing_group_relations g
+					   WHERE th.id = g.thing_id AND g.group_id = :group  %s;`, mq)
+
+	total, err := total(ctx, gr.db, cq, params)
+	if err != nil {
+		return groups.MemberPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	page := groups.MemberPage{
+		Members: items,
+		PageMetadata: groups.PageMetadata{
+			Total:  total,
+			Offset: offset,
+			Limit:  limit,
+		},
+	}
+
+	return page, nil
+}
+
+func (gr groupRepository) Memberships(ctx context.Context, userID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	m, mq, err := getGroupsMetadataQuery("thing_groups", gm)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errRetrieveDB, err)
+	}
+
+	if mq != "" {
+		mq = fmt.Sprintf("AND %s", mq)
+	}
+	q := fmt.Sprintf(`SELECT g.id, g.owner_id, g.parent_id, g.name, g.description, g.metadata 
+					  FROM thing_group_relations gr, thing_groups g
+					  WHERE gr.group_id = g.id and gr.thing_id = :userID 
+		  			  %s ORDER BY id LIMIT :limit OFFSET :offset;`, mq)
+
+	params := map[string]interface{}{
+		"userID":   userID,
+		"limit":    limit,
+		"offset":   offset,
+		"metadata": m,
+	}
+
+	rows, err := gr.db.NamedQueryContext(ctx, q, params)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+	defer rows.Close()
+
+	var items []groups.Group
+	for rows.Next() {
+		dbgr := dbGroup{}
+		if err := rows.StructScan(&dbgr); err != nil {
+			return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+		}
+		gr, err := toGroup(dbgr)
+		if err != nil {
+			return groups.GroupPage{}, err
+		}
+		items = append(items, gr)
+	}
+
+	cq := fmt.Sprintf(`SELECT COUNT(*) FROM thing_group_relations gr, thing_groups g
+					   WHERE gr.group_id = g.id and gr.thing_id = :userID %s;`, mq)
+
+	total, err := total(ctx, gr.db, cq, params)
+	if err != nil {
+		return groups.GroupPage{}, errors.Wrap(errSelectDb, err)
+	}
+
+	page := groups.GroupPage{
+		Groups: items,
+		PageMetadata: groups.PageMetadata{
+			Total:  total,
+			Offset: offset,
+			Limit:  limit,
+		},
+	}
+
+	return page, nil
+}
+
+func (gr groupRepository) Assign(ctx context.Context, thingID, groupID string) error {
+	dbr, err := toDBGroupRelation(thingID, groupID)
+	if err != nil {
+		return errors.Wrap(groups.ErrAssignToGroup, err)
+	}
+
+	qIns := `INSERT INTO thing_group_relations (group_id, thing_id) VALUES (:group_id, :thing_id)`
+	_, err = gr.db.NamedQueryContext(ctx, qIns, dbr)
+	if err != nil {
+		pqErr, ok := err.(*pq.Error)
+		if ok {
+			switch pqErr.Code.Name() {
+			case errInvalid, errTruncation:
+				return errors.Wrap(groups.ErrMalformedEntity, err)
+			case errDuplicate:
+				return errors.Wrap(groups.ErrGroupConflict, err)
+			case errFK:
+				return errors.Wrap(groups.ErrNotFound, err)
+			}
+		}
+		return errors.Wrap(groups.ErrAssignToGroup, err)
+	}
+
+	return nil
+}
+
+func (gr groupRepository) Unassign(ctx context.Context, userID, groupID string) error {
+	q := `DELETE FROM thing_group_relations WHERE thing_id = :thing_id AND group_id = :group_id`
+	dbr, err := toDBGroupRelation(userID, groupID)
+	if err != nil {
+		return errors.Wrap(groups.ErrNotFound, err)
+	}
+	if _, err := gr.db.NamedExecContext(ctx, q, dbr); err != nil {
+		return errors.Wrap(groups.ErrGroupConflict, err)
+	}
+	return nil
+}
+
+type dbGroup struct {
+	ID          uuid.NullUUID `db:"id"`
+	OwnerID     uuid.NullUUID `db:"owner_id"`
+	ParentID    uuid.NullUUID `db:"parent_id"`
+	Name        string        `db:"name"`
+	Description string        `db:"description"`
+	Metadata    dbMetadata    `db:"metadata"`
+	Level       int           `db:"level"`
+	Path        string        `db:"path"`
+	CreatedAt   time.Time     `db:"created_at"`
+	UpdatedAt   time.Time     `db:"updated_at"`
+}
+
+type dbGroupPage struct {
+	ID       uuid.NullUUID `db:"id"`
+	OwnerID  uuid.NullUUID `db:"owner_id"`
+	ParentID uuid.NullUUID `db:"parent_id"`
+	Metadata dbMetadata    `db:"metadata"`
+	Path     string        `db:"path"`
+	Level    uint64        `db:"level"`
+	Size     uint64        `db:"size"`
+}
+
+func toUUID(id string) (uuid.NullUUID, error) {
+	var uid uuid.NullUUID
+	if id == "" {
+		return uuid.NullUUID{UUID: uuid.Nil, Valid: false}, nil
+	}
+	err := uid.Scan(id)
+	return uid, err
+}
+
+func toString(id uuid.NullUUID) (string, error) {
+	if id.Valid {
+		return id.UUID.String(), nil
+	}
+	if id.UUID == uuid.Nil {
+		return "", nil
+	}
+	return "", errConvertingStringToUUID
+}
+
+func toDBGroup(g groups.Group) (dbGroup, error) {
+	parentID, err := toUUID(g.ParentID)
+	if err != nil {
+		return dbGroup{}, err
+	}
+	ownerID, err := toUUID(g.OwnerID)
+	if err != nil {
+		return dbGroup{}, err
+	}
+	groupID, err := toUUID(g.ID)
+	if err != nil {
+		return dbGroup{}, err
+	}
+
+	meta := dbMetadata(g.Metadata)
+
+	return dbGroup{
+		ID:          groupID,
+		Name:        g.Name,
+		ParentID:    parentID,
+		OwnerID:     ownerID,
+		Description: g.Description,
+		Metadata:    meta,
+		Path:        g.Path,
+		CreatedAt:   g.CreatedAt,
+		UpdatedAt:   g.UpdatedAt,
+	}, nil
+}
+
+func toDBGroupPage(ownerID, id, parentID, path string, level uint64, metadata groups.Metadata) (dbGroupPage, error) {
+	owner, err := toUUID(ownerID)
+	if err != nil {
+		return dbGroupPage{}, err
+	}
+
+	gid, err := toUUID(id)
+	if err != nil {
+		return dbGroupPage{}, err
+	}
+
+	parent, err := toUUID(parentID)
+	if err != nil {
+		return dbGroupPage{}, err
+	}
+	if err != nil {
+		return dbGroupPage{}, err
+	}
+	return dbGroupPage{
+		Metadata: dbMetadata(metadata),
+		ID:       gid,
+		OwnerID:  owner,
+		Level:    level,
+		Path:     path,
+		ParentID: parent,
+	}, nil
+}
+
+func toGroup(dbu dbGroup) (groups.Group, error) {
+	groupID, err := toString(dbu.ID)
+	if err != nil {
+		return groups.Group{}, err
+	}
+	parentID, err := toString(dbu.ParentID)
+	if err != nil {
+		return groups.Group{}, err
+	}
+	ownerID, err := toString(dbu.OwnerID)
+	if err != nil {
+		return groups.Group{}, err
+	}
+	return groups.Group{
+		ID:          groupID,
+		Name:        dbu.Name,
+		ParentID:    parentID,
+		OwnerID:     ownerID,
+		Description: dbu.Description,
+		Metadata:    groups.Metadata(dbu.Metadata),
+		Level:       dbu.Level,
+		Path:        dbu.Path,
+		UpdatedAt:   dbu.UpdatedAt,
+		CreatedAt:   dbu.CreatedAt,
+	}, nil
+}
+
+type dbGroupRelation struct {
+	Group uuid.UUID `db:"group_id"`
+	Thing uuid.UUID `db:"thing_id"`
+}
+
+func toDBGroupRelation(thingID, groupID string) (dbGroupRelation, error) {
+	grID, err := uuid.FromString(groupID)
+	if err != nil {
+		return dbGroupRelation{}, err
+	}
+	thID, err := uuid.FromString(thingID)
+	if err != nil {
+		return dbGroupRelation{}, err
+	}
+	return dbGroupRelation{
+		Group: grID,
+		Thing: thID,
+	}, nil
+}
+
+func getGroupsMetadataQuery(db string, m groups.Metadata) ([]byte, string, error) {
+	mq := ""
+	mb := []byte("{}")
+	if len(m) > 0 {
+		mq = db + `.metadata @> :metadata`
+		if db == "" {
+			mq = `metadata @> :metadata`
+		}
+
+		b, err := json.Marshal(m)
+		if err != nil {
+			return nil, "", err
+		}
+		mb = b
+	}
+	return mb, mq, nil
+}
+
+func processRows(rows *sqlx.Rows) ([]groups.Group, error) {
+	var items []groups.Group
+	for rows.Next() {
+		dbgr := dbGroup{}
+		if err := rows.StructScan(&dbgr); err != nil {
+			return items, errors.Wrap(errSelectDb, err)
+		}
+		gr, err := toGroup(dbgr)
+		if err != nil {
+			continue
+		}
+		items = append(items, gr)
+	}
+	return items, nil
+}
diff --git a/things/postgres/init.go b/things/postgres/init.go
index b49815f6..35c02188 100644
--- a/things/postgres/init.go
+++ b/things/postgres/init.go
@@ -93,6 +93,34 @@ func migrateDB(db *sqlx.DB) error {
 					 metadata TYPE JSONB using metadata::text::jsonb`,
 				},
 			},
+			{
+				Id: "things_4",
+				Up: []string{
+					`ALTER TABLE IF EXISTS things ADD CONSTRAINT things_id_key UNIQUE (id)`,
+					`CREATE extension LTREE`,
+					`CREATE TABLE IF NOT EXISTS thing_groups ( 
+						id          UUID UNIQUE NOT NULL,
+						parent_id   UUID, 
+						owner_id    UUID,
+						name        VARCHAR(254) NOT NULL,
+						description VARCHAR(1024),
+						metadata    JSONB,
+						path        LTREE, 
+						created_at  TIMESTAMPTZ,
+						updated_at  TIMESTAMPTZ,
+						PRIMARY KEY (owner_id, path),
+						FOREIGN KEY (parent_id) REFERENCES thing_groups (id) ON DELETE CASCADE ON UPDATE CASCADE
+				   )`,
+					`CREATE TABLE IF NOT EXISTS thing_group_relations (
+						thing_id UUID NOT NULL,
+						group_id UUID NOT NULL,
+						FOREIGN KEY (thing_id) REFERENCES things (id) ON DELETE CASCADE ON UPDATE CASCADE,
+						FOREIGN KEY (group_id) REFERENCES thing_groups (id) ON DELETE CASCADE ON UPDATE CASCADE,
+						PRIMARY KEY (thing_id, group_id)
+				   )`,
+					`CREATE INDEX path_gist_idx ON thing_groups USING GIST (path);`,
+				},
+			},
 		},
 	}
 
diff --git a/things/postgres/things.go b/things/postgres/things.go
index 7507a922..3cc9ce7f 100644
--- a/things/postgres/things.go
+++ b/things/postgres/things.go
@@ -22,6 +22,11 @@ const (
 	errTruncation = "string_data_right_truncation"
 )
 
+var (
+	errUpdateDB   = errors.New("failed to update db")
+	errRetrieveDB = errors.New("failed retrieving from db")
+)
+
 var _ things.ThingRepository = (*thingRepository)(nil)
 
 type thingRepository struct {
@@ -184,8 +189,7 @@ func (tr thingRepository) RetrieveAll(ctx context.Context, owner string, offset,
 	}
 
 	q := fmt.Sprintf(`SELECT id, name, key, metadata FROM things
-		  WHERE owner = :owner %s%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq, nq)
-
+		  			  WHERE owner = :owner %s%s ORDER BY id LIMIT :limit OFFSET :offset;`, mq, nq)
 	params := map[string]interface{}{
 		"owner":    owner,
 		"limit":    limit,
diff --git a/things/redis/streams.go b/things/redis/streams.go
index ab189930..1ae324c4 100644
--- a/things/redis/streams.go
+++ b/things/redis/streams.go
@@ -7,6 +7,7 @@ import (
 	"context"
 
 	"github.com/go-redis/redis"
+	"github.com/mainflux/mainflux/internal/groups"
 	"github.com/mainflux/mainflux/things"
 )
 
@@ -239,3 +240,47 @@ func (es eventStore) CanAccessByID(ctx context.Context, chanID string, thingID s
 func (es eventStore) Identify(ctx context.Context, key string) (string, error) {
 	return es.svc.Identify(ctx, key)
 }
+
+func (es eventStore) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
+	return es.svc.CreateGroup(ctx, token, g)
+}
+
+func (es eventStore) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
+	return es.svc.ViewGroup(ctx, token, id)
+}
+
+func (es eventStore) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	return es.svc.ListGroups(ctx, token, level, gm)
+}
+
+func (es eventStore) ListParents(ctx context.Context, token, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	return es.svc.ListParents(ctx, token, childID, level, gm)
+}
+
+func (es eventStore) ListChildren(ctx context.Context, token, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	return es.svc.ListChildren(ctx, token, parentID, level, gm)
+}
+
+func (es eventStore) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
+	return es.svc.ListMembers(ctx, token, groupID, offset, limit, gm)
+}
+
+func (es eventStore) RemoveGroup(ctx context.Context, token, id string) error {
+	return es.svc.RemoveGroup(ctx, token, id)
+}
+
+func (es eventStore) Unassign(ctx context.Context, token, memberID, groupID string) error {
+	return es.svc.Unassign(ctx, token, memberID, groupID)
+}
+
+func (es eventStore) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
+	return es.svc.UpdateGroup(ctx, token, g)
+}
+
+func (es eventStore) Assign(ctx context.Context, token, memberID, groupID string) error {
+	return es.svc.Assign(ctx, token, memberID, groupID)
+}
+
+func (es eventStore) ListMemberships(ctx context.Context, token, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	return es.svc.ListMemberships(ctx, token, memberID, offset, limit, gm)
+}
diff --git a/things/service.go b/things/service.go
index 7641aa17..a27a5d44 100644
--- a/things/service.go
+++ b/things/service.go
@@ -6,11 +6,15 @@ package things
 import (
 	"context"
 
+	"github.com/mainflux/mainflux/internal/groups"
 	"github.com/mainflux/mainflux/pkg/errors"
 
 	"github.com/mainflux/mainflux"
+	uuidProvider "github.com/mainflux/mainflux/pkg/uuid"
 )
 
+const things = "things"
+
 var (
 	// ErrUnauthorizedAccess indicates missing or invalid credentials provided
 	// when accessing a protected resource.
@@ -36,6 +40,12 @@ var (
 
 	// ErrDisconnect indicates error in removing connection
 	ErrDisconnect = errors.New("remove connection failed")
+
+	// ErrCreateGroup indicates error in creating group.
+	ErrCreateGroup = errors.New("failed to create group")
+
+	// ErrFailedToRetrieveThings failed to retrieve things.
+	ErrFailedToRetrieveThings = errors.New("failed to retrieve group members")
 )
 
 // Service specifies an API that must be fullfiled by the domain service
@@ -110,6 +120,8 @@ type Service interface {
 
 	// Identify returns thing ID for given thing key.
 	Identify(ctx context.Context, key string) (string, error)
+
+	groups.Service
 }
 
 // PageMetadata contains page metadata that helps navigation.
@@ -126,16 +138,18 @@ type thingsService struct {
 	auth         mainflux.AuthNServiceClient
 	things       ThingRepository
 	channels     ChannelRepository
+	groups       groups.Repository
 	channelCache ChannelCache
 	thingCache   ThingCache
 	uuidProvider mainflux.UUIDProvider
 }
 
 // New instantiates the things service implementation.
-func New(auth mainflux.AuthNServiceClient, things ThingRepository, channels ChannelRepository, ccache ChannelCache, tcache ThingCache, up mainflux.UUIDProvider) Service {
+func New(auth mainflux.AuthNServiceClient, things ThingRepository, channels ChannelRepository, groups groups.Repository, ccache ChannelCache, tcache ThingCache, up mainflux.UUIDProvider) Service {
 	return &thingsService{
 		auth:         auth,
 		things:       things,
+		groups:       groups,
 		channels:     channels,
 		channelCache: ccache,
 		thingCache:   tcache,
@@ -382,3 +396,109 @@ func (ts *thingsService) hasThing(ctx context.Context, chanID, thingKey string)
 	}
 	return thingID, nil
 }
+
+func (ts *thingsService) CreateGroup(ctx context.Context, token string, g groups.Group) (string, error) {
+	user, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token})
+	if err != nil {
+		return "", errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+
+	uid, err := uuidProvider.New().ID()
+	if err != nil {
+		return "", errors.Wrap(ErrCreateGroup, err)
+	}
+
+	g.ID = uid
+	g.OwnerID = user.GetId()
+	if _, err := ts.groups.Save(ctx, g); err != nil {
+		return "", err
+	}
+
+	return g.ID, nil
+}
+
+func (ts *thingsService) ListGroups(ctx context.Context, token string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.RetrieveAll(ctx, level, gm)
+
+}
+
+func (ts *thingsService) ListParents(ctx context.Context, token string, childID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.RetrieveAllParents(ctx, childID, level, gm)
+}
+
+func (ts *thingsService) ListChildren(ctx context.Context, token string, parentID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.RetrieveAllChildren(ctx, parentID, level, gm)
+}
+
+func (ts *thingsService) ListMembers(ctx context.Context, token, groupID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	p, err := ts.groups.Members(ctx, groupID, offset, limit, gm)
+	if err != nil {
+		return groups.MemberPage{}, errors.Wrap(ErrFailedToRetrieveThings, err)
+	}
+	mp := groups.MemberPage{
+		PageMetadata: groups.PageMetadata{
+			Total:  p.Total,
+			Offset: p.Offset,
+			Limit:  p.Limit,
+			Name:   things,
+		},
+		Members: make([]groups.Member, 0),
+	}
+	mp.Members = append(mp.Members, p.Members)
+	return mp, nil
+}
+
+func (ts *thingsService) RemoveGroup(ctx context.Context, token, id string) error {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.Delete(ctx, id)
+}
+
+func (ts *thingsService) Unassign(ctx context.Context, token, memberID, groupID string) error {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.Unassign(ctx, memberID, groupID)
+}
+
+func (ts *thingsService) UpdateGroup(ctx context.Context, token string, g groups.Group) (groups.Group, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+
+	return ts.groups.Update(ctx, g)
+}
+
+func (ts *thingsService) ViewGroup(ctx context.Context, token, id string) (groups.Group, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.RetrieveByID(ctx, id)
+}
+
+func (ts *thingsService) Assign(ctx context.Context, token, memberID, groupID string) error {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.Assign(ctx, memberID, groupID)
+}
+
+func (ts *thingsService) ListMemberships(ctx context.Context, token string, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	if _, err := ts.auth.Identify(ctx, &mainflux.Token{Value: token}); err != nil {
+		return groups.GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
+	}
+	return ts.groups.Memberships(ctx, memberID, offset, limit, gm)
+}
diff --git a/things/tracing/groups.go b/things/tracing/groups.go
new file mode 100644
index 00000000..bb623b84
--- /dev/null
+++ b/things/tracing/groups.go
@@ -0,0 +1,130 @@
+// Copyright (c) Mainflux
+// SPDX-License-Identifier: Apache-2.0
+
+// Package tracing contains middlewares that will add spans to existing traces.
+package tracing
+
+import (
+	"context"
+
+	"github.com/mainflux/mainflux/internal/groups"
+	opentracing "github.com/opentracing/opentracing-go"
+)
+
+const (
+	assign               = "assign"
+	saveGroup            = "save_group"
+	deleteGroup          = "delete_group"
+	updateGroup          = "update_group"
+	retrieveByID         = "retrieve_by_id"
+	retrieveAllAncestors = "retrieve_all_ancestors"
+	retrieveAllChildren  = "retrieve_all_children"
+	retrieveAll          = "retrieve_all_groups"
+	retrieveByName       = "retrieve_by_name"
+	memberships          = "memberships"
+	members              = "members"
+	unassign             = "unassign"
+)
+
+var _ groups.Repository = (*groupRepositoryMiddleware)(nil)
+
+type groupRepositoryMiddleware struct {
+	tracer opentracing.Tracer
+	repo   groups.Repository
+}
+
+// GroupRepositoryMiddleware tracks request and their latency, and adds spans to context.
+func GroupRepositoryMiddleware(tracer opentracing.Tracer, gr groups.Repository) groups.Repository {
+	return groupRepositoryMiddleware{
+		tracer: tracer,
+		repo:   gr,
+	}
+}
+
+func (grm groupRepositoryMiddleware) Save(ctx context.Context, g groups.Group) (groups.Group, error) {
+	span := createSpan(ctx, grm.tracer, saveGroup)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Save(ctx, g)
+}
+
+func (grm groupRepositoryMiddleware) Update(ctx context.Context, g groups.Group) (groups.Group, error) {
+	span := createSpan(ctx, grm.tracer, updateGroup)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Update(ctx, g)
+}
+
+func (grm groupRepositoryMiddleware) Delete(ctx context.Context, groupID string) error {
+	span := createSpan(ctx, grm.tracer, deleteGroup)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Delete(ctx, groupID)
+}
+
+func (grm groupRepositoryMiddleware) RetrieveByID(ctx context.Context, id string) (groups.Group, error) {
+	span := createSpan(ctx, grm.tracer, retrieveByID)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.RetrieveByID(ctx, id)
+}
+
+func (grm groupRepositoryMiddleware) RetrieveAllParents(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	span := createSpan(ctx, grm.tracer, retrieveAllAncestors)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.RetrieveAllParents(ctx, groupID, level, gm)
+}
+
+func (grm groupRepositoryMiddleware) RetrieveAllChildren(ctx context.Context, groupID string, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	span := createSpan(ctx, grm.tracer, retrieveAllChildren)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.RetrieveAllChildren(ctx, groupID, level, gm)
+}
+
+func (grm groupRepositoryMiddleware) RetrieveAll(ctx context.Context, level uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	span := createSpan(ctx, grm.tracer, retrieveAll)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.RetrieveAll(ctx, level, gm)
+}
+
+func (grm groupRepositoryMiddleware) Memberships(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.GroupPage, error) {
+	span := createSpan(ctx, grm.tracer, memberships)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Memberships(ctx, memberID, offset, limit, gm)
+}
+
+func (grm groupRepositoryMiddleware) Members(ctx context.Context, memberID string, offset, limit uint64, gm groups.Metadata) (groups.MemberPage, error) {
+	span := createSpan(ctx, grm.tracer, members)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Members(ctx, memberID, offset, limit, gm)
+}
+
+func (grm groupRepositoryMiddleware) Unassign(ctx context.Context, memberID, groupID string) error {
+	span := createSpan(ctx, grm.tracer, unassign)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Unassign(ctx, memberID, groupID)
+}
+
+func (grm groupRepositoryMiddleware) Assign(ctx context.Context, memberID, groupID string) error {
+	span := createSpan(ctx, grm.tracer, assign)
+	defer span.Finish()
+	ctx = opentracing.ContextWithSpan(ctx, span)
+
+	return grm.repo.Assign(ctx, memberID, groupID)
+}
diff --git a/users/service.go b/users/service.go
index 54361cc2..951c9c34 100644
--- a/users/service.go
+++ b/users/service.go
@@ -330,23 +330,18 @@ func (svc usersService) CreateGroup(ctx context.Context, token string, group Gro
 		return Group{}, ErrMalformedEntity
 	}
 
-	email, err := svc.identify(ctx, token)
+	identity, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
 	if err != nil {
 		return Group{}, err
 	}
 
-	user, err := svc.users.RetrieveByEmail(ctx, email)
-	if err != nil {
-		return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
-	}
-
 	uid, err := uuidProvider.New().ID()
 	if err != nil {
 		return Group{}, errors.Wrap(ErrCreateUser, err)
 	}
 
 	group.ID = uid
-	group.OwnerID = user.ID
+	group.OwnerID = identity.GetId()
 
 	return svc.groups.Save(ctx, group)
 }
@@ -418,9 +413,9 @@ func (svc usersService) issue(ctx context.Context, id, email string, keyType uin
 }
 
 func (svc usersService) identify(ctx context.Context, token string) (string, error) {
-	email, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
+	identity, err := svc.auth.Identify(ctx, &mainflux.Token{Value: token})
 	if err != nil {
 		return "", errors.Wrap(ErrUnauthorizedAccess, err)
 	}
-	return email.GetEmail(), nil
+	return identity.GetEmail(), nil
 }


Test patch:
diff --git a/authn/postgres/setup_test.go b/authn/postgres/setup_test.go
index ee40e2b0..98acfd30 100644
--- a/authn/postgres/setup_test.go
+++ b/authn/postgres/setup_test.go
@@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}
diff --git a/bootstrap/postgres/setup_test.go b/bootstrap/postgres/setup_test.go
index 43b3581a..5e07c80f 100644
--- a/bootstrap/postgres/setup_test.go
+++ b/bootstrap/postgres/setup_test.go
@@ -36,7 +36,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}
diff --git a/certs/postgres/setup_test.go b/certs/postgres/setup_test.go
index a19cdabb..08d72352 100644
--- a/certs/postgres/setup_test.go
+++ b/certs/postgres/setup_test.go
@@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		testLog.Error(fmt.Sprintf("Could not start container: %s", err))
 	}
diff --git a/pkg/sdk/go/things_test.go b/pkg/sdk/go/things_test.go
index 44b18c46..30d754f3 100644
--- a/pkg/sdk/go/things_test.go
+++ b/pkg/sdk/go/things_test.go
@@ -49,7 +49,7 @@ func newThingsService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
 
 func newThingsServer(svc things.Service) *httptest.Server {
diff --git a/readers/postgres/setup_test.go b/readers/postgres/setup_test.go
index 3f3b7e74..e8281cd6 100644
--- a/readers/postgres/setup_test.go
+++ b/readers/postgres/setup_test.go
@@ -38,7 +38,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}
diff --git a/things/api/auth/grpc/setup_test.go b/things/api/auth/grpc/setup_test.go
index aac3a1dc..7307f172 100644
--- a/things/api/auth/grpc/setup_test.go
+++ b/things/api/auth/grpc/setup_test.go
@@ -50,5 +50,5 @@ func newService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
diff --git a/things/api/auth/http/endpoint_test.go b/things/api/auth/http/endpoint_test.go
index babba7e5..735b942b 100644
--- a/things/api/auth/http/endpoint_test.go
+++ b/things/api/auth/http/endpoint_test.go
@@ -75,7 +75,7 @@ func newService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
 
 func newServer(svc things.Service) *httptest.Server {
diff --git a/things/api/things/http/endpoint_test.go b/things/api/things/http/endpoint_test.go
index 7593f359..65764eeb 100644
--- a/things/api/things/http/endpoint_test.go
+++ b/things/api/things/http/endpoint_test.go
@@ -80,7 +80,7 @@ func newService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
 
 func newServer(svc things.Service) *httptest.Server {
diff --git a/things/postgres/setup_test.go b/things/postgres/setup_test.go
index b3189058..a31a2652 100644
--- a/things/postgres/setup_test.go
+++ b/things/postgres/setup_test.go
@@ -38,7 +38,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}
diff --git a/things/redis/streams_test.go b/things/redis/streams_test.go
index 92672c72..95c3694b 100644
--- a/things/redis/streams_test.go
+++ b/things/redis/streams_test.go
@@ -47,7 +47,7 @@ func newService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
 
 func TestCreateThings(t *testing.T) {
diff --git a/things/service_test.go b/things/service_test.go
index 296d427a..69706d30 100644
--- a/things/service_test.go
+++ b/things/service_test.go
@@ -38,7 +38,7 @@ func newService(tokens map[string]string) things.Service {
 	thingCache := mocks.NewThingCache()
 	uuidProvider := uuid.NewMock()
 
-	return things.New(auth, thingsRepo, channelsRepo, chanCache, thingCache, uuidProvider)
+	return things.New(auth, thingsRepo, channelsRepo, nil, chanCache, thingCache, uuidProvider)
 }
 
 func TestCreateThings(t *testing.T) {
diff --git a/users/postgres/setup_test.go b/users/postgres/setup_test.go
index c9953269..7aa58ae2 100644
--- a/users/postgres/setup_test.go
+++ b/users/postgres/setup_test.go
@@ -30,7 +30,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}
diff --git a/writers/postgres/setup_test.go b/writers/postgres/setup_test.go
index d6363174..f5edd656 100644
--- a/writers/postgres/setup_test.go
+++ b/writers/postgres/setup_test.go
@@ -38,7 +38,7 @@ func TestMain(m *testing.M) {
 		"POSTGRES_PASSWORD=test",
 		"POSTGRES_DB=test",
 	}
-	container, err := pool.Run("postgres", "10.2-alpine", cfg)
+	container, err := pool.Run("postgres", "10.8-alpine", cfg)
 	if err != nil {
 		log.Fatalf("Could not start container: %s", err)
 	}