Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/dataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/go-challenge.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

209 changes: 209 additions & 0 deletions Arman.postman_collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{
"info": {
"_postman_id": "f726f960-6fa9-4363-83e6-bda1f789aace",
"name": "Arman",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "13021723"
},
"item": [
{
"name": "Create User Segment",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"userId\" : \"7db7fd9b-4378-492f-b624-907c0c21b506\",\r\n \"segment\" : \"segment\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "localhost:8080/api/v1/user-segments",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"v1",
"user-segments"
]
}
},
"response": [
{
"name": "Create User Segment",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"userId\" : \"7db7fd9b-4378-492f-b624-907c0c21b506\",\r\n \"segment\" : \"segment\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "localhost:8080/api/v1/user-segments",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"v1",
"user-segments"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Sat, 10 Sep 2022 13:16:05 GMT"
},
{
"key": "Content-Length",
"value": "230"
}
],
"cookie": [],
"body": "{\n \"response\": {\n \"error\": false,\n \"message\": \"user segment created\",\n \"data\": {\n \"Id\": \"7db7fd9b-4378-492f-b624-907c0c21b506\",\n \"Segment\": \"segment\",\n \"CreatedAt\": \"2022-09-10T17:46:05.6724989+04:30\",\n \"UpdatedAt\": \"2022-09-10T17:46:05.6724989+04:30\"\n }\n }\n}"
}
]
},
{
"name": "CountByTitle",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "localhost:8080/api/v1/user-segments/count-segments?title=segment",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"v1",
"user-segments",
"count-segments"
],
"query": [
{
"key": "title",
"value": "segment"
}
]
}
},
"response": [
{
"name": "CountByTitle",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
"raw": "localhost:8080/api/v1/user-segments/count-segments?title=segment",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"v1",
"user-segments",
"count-segments"
],
"query": [
{
"key": "title",
"value": "segment"
}
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Sat, 10 Sep 2022 13:16:18 GMT"
},
{
"key": "Content-Length",
"value": "78"
}
],
"cookie": [],
"body": "{\n \"response\": {\n \"error\": false,\n \"message\": \"\",\n \"data\": {\n \"title\": \"segment\",\n \"count\": 1\n }\n }\n}"
}
]
},
{
"name": "Scheduled",
"request": {
"method": "GET",
"header": []
},
"response": [
{
"name": "Scheduled",
"originalRequest": {
"method": "POST",
"header": [],
"url": {
"raw": "localhost:8080/api/v1/cron-jobs/user-segments/remove-segment",
"host": [
"localhost"
],
"port": "8080",
"path": [
"api",
"v1",
"cron-jobs",
"user-segments",
"remove-segment"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Date",
"value": "Sat, 10 Sep 2022 13:16:33 GMT"
},
{
"key": "Content-Length",
"value": "75"
}
],
"cookie": [],
"body": "{\n \"response\": {\n \"error\": false,\n \"message\": \"\",\n \"data\": {\n \"JobStatus\": \"Successful\"\n }\n }\n}"
}
]
}
]
}
102 changes: 62 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,62 @@
# How many users? (Go Challenge)

Suppose there is a "User Segmentation Service" (USS) that segments users based on their activities.
For example, if a user visits sports news, USS classifies and tags "sport" to him.
So we have a pair: (the user_id, and the segment) for example, (u104010, "sports").

We want to develop an Estimation Service (ES) that interacts with USS directly.
ES receives the pair from USS as input and stores it.
The responsibility of ES is to answer a simple query: "How many users exist on a specific segment?".
For example, "how many users are in the sports segment?".

![](https://raw.githubusercontent.com/ArmanCreativeSolutions/go-challenge/main/Untitled%20Diagram.drawio.png?raw=true)

The query is simple, but two assumptions may make it a little challenging:
- A specific user remains just two weeks on a segment. After that,
we should not count "u104010" on the sports segment.
- There are millions of users and hundreds of segments. So your solution(s) must be scalable


## Requirements

- Implement a (REST API, RESTful API, soap, Graphql, RPC, gRPC, or whatever protocol you prefer)
interface to receive data (user_id, segment pair) from USS.
- Implement a method to estimate the number of users in a specific segment. ( `func estimate(segment) -> number of users`)

## Implementation details

Try to write your code as reusable and readable as possible.
Also, don't forget to document your code and clear the reasons for all your decisions in the code.

If your solution is not simple enough for implementing fast, you can just describe it in your documents.

Use any tools that you prefer just explain the reason of choices in your documents.
For example explain why you choose REST API for receiving data.

It is more valuable to us that the project comes with unit tests.

Please fork this repository and add your code to that.
Don't forget that your commits are so important.
So be sure that you're committing your code often with a proper commit message.
# Arman Go Challenge
This project using Domain Driven Design handles request from user segment service using:
* [**Gin Gonic**](https://github.com/gin-gonic/gin)
* [**Gorm**](https://github.com/go-gorm/gorm)
* [**Sqlite**](https://sqlite.org)

I use gin framework in order to make application communicate with
REST api, and Sqlite because our data is structured, and also for its simplicity.
I use gorm since I prefer to use ORMs in terms of safety
and more control over data programmatically. Also, I have chosen REST api
because it allows great variety of data formats, which make services
more flexible, and I have prior proficiency in working with REST.

## Terminology
- __User Segment__ — User segment is the main entity of this project. in each record I store user id which is considers as a UUID and the segment related to that user which has string format.

## Structure of the Code

```
┌───┐
│ / │
└─┬─┘
├───────▶ application ───▶ user-segments ─ ─ ─ ─ related service
│ │
│ └──▶ dto ─ ─ ─ ─ data transfer object to recieve and map requests to models and vice versa
├───────▶ domain ─ ─ ─ ─ models are defined here
│ │
│ └──▶ usersegments ─ ─ ─ ─ User Segment Model
├───────▶ infrastructure ─ ─ ─ ─ ─ Configs, general models (validations, response, errors, etc) and everything that the whole application uses
│ │
│ └──▶ dbconfig ─ ─ ─ ─ ─ Database configurations
│ │
│ └──▶ exception ─ ─ ─ ─ ─ General models for error handling
│ │
│ └──▶ repository ─ ─ ─ ─ ─ Repositories to communicate with database
│ │
│ └──▶ response ─ ─ ─ ─ ─ General models for application responses
│ │
│ └──▶ routes ─ ─ ─ ─ ─ Api routing setup
│ │
│ └──▶ validation ─ ─ ─ ─ ─ General validation for requests
├───────▶ presentation ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ Controller layer
│ │
│ └──▶ cronjobs ─ ─ ─ ─ ─ ─ ─ ─ ─ Define cron jobs
│ │
│ └──▶ usersegmentcontroller ─ ─ ─ Expose apis
```

## Setting up the environment
The system needs to have sqlite database. database name is hardcoded in sqlite-config.go as a constant variable. it is removed and created with each run.
also, application runs on localhost:8080 which is also hardcoded in main.go.

## Current issues
- It is better to use a separate environment file to avoid hardcode database name and port address.
- I tried to dockerized this project with a proper makefile, but the whole application package structure needed to be changed.
- I prefer to put repository next to the related model in domain package (I have done this practice in Java), however, since I had to do dependency injection without an IoC container (like Spring boot), I faced a circular dependency, so I had to move the repository from domain to infrastructure. I think the best solution is to have a repository interface and implement it in each repository.
- Use a middleware for logging could be a better practice.
- I could not find a proper practice in order to implement cron jobs in a gin context, so, I designed a REST client to call a function every 14 days.
17 changes: 17 additions & 0 deletions application/user-segments/dto/user-segment-dtos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dto

import "github.com/google/uuid"

type CreateUserSegmentDTO struct {
UserId uuid.UUID `json:"userId" validate:"required"`
Segment string `json:"segment" validate:"required"`
}

type CountUserSegmentResponse struct {
Title string `json:"title"`
Count int64 `json:"count"`
}

type RemoveSegmentCronJobResponse struct {
JobStatus string
}
Loading