-
Notifications
You must be signed in to change notification settings - Fork 0
Implement user favourites service (Go challenge) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
39b86d4
32038ec
30f2326
118e0e2
bac59cd
073403e
44d23cd
31ac8cf
771d5cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Build stage | ||
| FROM golang:1.25-alpine AS builder | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY go.mod ./ | ||
| COPY go.sum ./ | ||
| RUN go mod download | ||
| COPY . . | ||
|
|
||
| RUN go build -o favourites main.go | ||
|
|
||
| # Runtime stage | ||
| FROM alpine:3.22 | ||
| RUN apk --no-cache add ca-certificates | ||
|
|
||
| WORKDIR /root/ | ||
| COPY --from=builder /app/favourites . | ||
|
|
||
| EXPOSE 8080 | ||
| CMD ["./favourites"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,121 @@ | ||
| # GlobalWebIndex Engineering Challenge | ||
| # User Favourites Service | ||
|
|
||
| ## Introduction | ||
| ## Scope | ||
| A web server that lets users manage their favourite assets including charts, insights and audiences. | ||
| Supports listing, adding, removing and editing favourites. | ||
|
|
||
| This challenge is designed to give you the opportunity to demonstrate your abilities as a software engineer and specifically your knowledge of the Go language. | ||
| ## How to Run | ||
|
|
||
| On the surface the challenge is trivial to solve, however you should choose to add features or capabilities which you feel demonstrate your skills and knowledge the best. For example, you could choose to optimise for performance and concurrency, you could choose to add a robust security layer or ensure your application is highly available. Or all of these. | ||
| ### Run locally | ||
| To build and run the program: | ||
| ```bash | ||
| go run main.go | ||
| ``` | ||
| Or, if you want to build first: | ||
| ```bash | ||
| go build -o favourites | ||
| ./favourites | ||
| ``` | ||
|
|
||
| Of course, usually we would choose to solve any given requirement with the simplest possible solution, however that is not the spirit of this challenge. | ||
| ### Run with Docker | ||
| ```bash | ||
| docker build -t favourites . | ||
| docker run --rm -p 8080:8080 favourites | ||
| ``` | ||
|
|
||
| ## Challenge | ||
| ## How to Test | ||
| Tests cover basic functionality and a few error scenarios. | ||
|
|
||
| Let's say that in GWI platform all of our users have access to a huge list of assets. We want our users to have a peronal list of favourites, meaning assets that favourite or “star” so that they have them in their frontpage dashboard for quick access. An asset can be one the following | ||
| * Chart (that has a small title, axes titles and data) | ||
| * Insight (a small piece of text that provides some insight into a topic, e.g. "40% of millenials spend more than 3hours on social media daily") | ||
| * Audience (which is a series of characteristics, for that exercise lets focus on gender (Male, Female), birth country, age groups, hours spent daily on social media, number of purchases last month) | ||
| e.g. Males from 24-35 that spent more than 3 hours on social media daily. | ||
| Run them with: | ||
| ```bash | ||
| go test ./... | ||
| ``` | ||
|
|
||
| Build a web server which has some endpoint to receive a user id and return a list of all the user’s favourites. Also we want endpoints that would add an asset to favourites, remove it, or edit its description. Assets obviously can share some common attributes (like their description) but they also have completely different structure and data. It’s up to you to decide the structure and we are not looking for something overly complex here (especially for the cases of audiences). There is no need to have/deploy/create an actual database although we would like to discuss about storage options and data representations. | ||
| ## Usage examples | ||
| - Add a favourite | ||
| ```bash | ||
| curl -X POST -H "Content-Type: application/json" \ | ||
| -d '{"assetId":"chart-42", "assetType":"chart", "description":"Top sales", "assetData":{"title":"Sales Q4","axes":["month","revenue"]}}' \ | ||
| http://localhost:8080/favourites/user123 | ||
| ``` | ||
| `Response 201, with created entry including generated id` | ||
|
|
||
| Note that users have no limit on how many assets they want on their favourites so your service will need to provide a reasonable response time. | ||
| ``` | ||
| { | ||
| "id": "fav-1", | ||
| "userId": "user123", | ||
| "assetId": "chart-42", | ||
| "assetType": "chart", | ||
| "description": "Top sales", | ||
| "assetData": { "title": "Sales Q4", "axes": ["month","revenue"] }, | ||
| "createdAt": "2025-09-11T17:18:53.9766385+03:00", | ||
| "updatedAt": "2025-09-11T17:18:53.9766385+03:00" | ||
| } | ||
| ``` | ||
|
|
||
| A working server application with functional API is required, along with a clear readme.md. Useful and passing tests would be also be viewed favourably | ||
| - List favourites for a user | ||
| ```bash | ||
| curl -s http://localhost:8080/favourites/user123 | jq | ||
| ``` | ||
| `Response 200` | ||
|
|
||
| It is appreciated, though not required, if a Dockerfile is included. | ||
| ``` | ||
| [ | ||
| { | ||
| "id": "fav-1", | ||
| "userId": "user123", | ||
| "assetId": "chart-42", | ||
| "assetType": "chart", | ||
| "description": "Top sales", | ||
| "assetData": { "title": "Sales Q4", "axes": ["month","revenue"] }, | ||
| "createdAt": "2025-09-11T17:18:53.9766385+03:00", | ||
| "updatedAt": "2025-09-11T17:18:53.9766385+03:00" | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| ## Submission | ||
| - Update a favourite | ||
| ```bash | ||
| curl -X PUT -H "Content-Type: application/json" \ | ||
| -d '{"assetId":"chart-21", "assetType":"chart", "description":"Sales frequency", "assetData":{"title":"Annual retention","axes":["month","invoice_count"]}}' \ | ||
| http://localhost:8080/favourites/user123/fav-1 | ||
| ``` | ||
|
|
||
| Just create a fork from the current repo and send it to us! | ||
| - Delete a favourite | ||
| ```bash | ||
| curl -X DELETE http://localhost:8080/favourites/user123/fav-1 | ||
| ``` | ||
|
|
||
| Good luck, potential colleague! | ||
| ## Assumptions | ||
| - REST API endpoints to fetch, add, remove and update assets in favourites list. | ||
| - JSON request/response with lower-camel JSON keys (id, userId, assetId, assetType, description, assetData, createdAt, updatedAt). | ||
| - In-memory store for the challenge purposes. No data store persistence present. Stored data are lost on restart. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would you model it in a database? What DB would you use and with what schema?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For persistence, I would align with the platform’s existing DB solution. CREATE TABLE favourites (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
asset_id TEXT NOT NULL,
asset_type TEXT NOT NULL,
description TEXT,
asset_data JSONB NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(user_id, asset_id)
);
|
||
| - Tests cover store methods, HTTP handlers, validation, conflicts and error paths (including with a mock store). | ||
|
|
||
| ## Data model | ||
| - This service stores references to existing platform assets. The source of truth for assets lives upstream. | ||
| - `description` is a user-provided label for the favourite and may be empty. | ||
| - `assetData` stores the upstream asset JSON as-is (flexible); on updates, clients send the full updated asset JSON. The service does not validate against upstream schemas. | ||
| - On reads, the service returns stored favourites. If upstream assets are missing, clients are expected to avoid selecting them; future versions may exclude or flag missing assets when an upstream lookup is enabled. | ||
|
|
||
| ## CI/CD (suggested checks) | ||
| In a CI pipeline, you can run basic quality gates before building and deploying: | ||
|
|
||
| ```bash | ||
| # Format code | ||
| go fmt ./... | ||
|
|
||
| # Static analysis | ||
| go vet ./... | ||
| staticcheck ./... | ||
|
|
||
| # Run tests | ||
| go test ./... | ||
| ``` | ||
|
|
||
| ## Next steps | ||
| - Solution is based on an in-memory store which makes storage ephemeral and works for a single instance only. Persistent storage should be used, for instance Postgres JSONB, or adopt a platform-wide storage solution. | ||
| - Make use of request context for better efficiency, especially when a persistent store is integrated. | ||
| - Coordinate upstream contracts for asset verification and potential reconciliation (exclusion/flags for missing assets) once teams align. | ||
| - Performance: caching per user with Redis since it is shown on the front page. | ||
| - Performance: pagination for very large lists. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| module github.com/parath/platform-go-challenge | ||
|
|
||
| go 1.25 | ||
|
|
||
| require github.com/gorilla/mux v1.8.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= | ||
| github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package favourites | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "time" | ||
| ) | ||
|
|
||
| type AssetType string | ||
|
|
||
| const ( | ||
| AssetTypeChart AssetType = "chart" | ||
| AssetTypeInsight AssetType = "insight" | ||
| AssetTypeAudience AssetType = "audience" | ||
| ) | ||
|
|
||
| // Favourite represents a user's favourite asset. | ||
| // Common attributes (ID, UserID, AssetID, AssetType, Description, CreatedAt) | ||
| // are top-level fields. Asset-specific data (like chart axes or audience criteria) | ||
| // is stored as free-form JSON in AssetData for flexibility. | ||
| type Favourite struct { | ||
| ID string `json:"id"` | ||
| UserID string `json:"userId"` | ||
| AssetID string `json:"assetId"` | ||
| AssetType AssetType `json:"assetType"` | ||
| Description string `json:"description,omitempty"` | ||
| AssetData json.RawMessage `json:"assetData"` | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What’s the reason behind opting for
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great question! I chose |
||
| CreatedAt time.Time `json:"createdAt"` | ||
| UpdatedAt time.Time `json:"updatedAt"` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package favourites | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
|
||
| // Store defines the behaviour for managing favourites. | ||
| type Store interface { | ||
| AddFavourite(Favourite) (Favourite, error) | ||
| GetFavourites(userID string) ([]Favourite, error) | ||
| UpdateFavourite(userID, favouriteID string, update Favourite) (Favourite, error) | ||
| DeleteFavourite(userID, favouriteID string) error | ||
| NextFavouriteID() string | ||
| } | ||
|
|
||
| // InMemoryStore stores favourites in memory per user | ||
| type InMemoryStore struct { | ||
| mu sync.RWMutex | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great to see that you use a RW mutex 👏
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙏 Thanks! |
||
| store map[string][]Favourite | ||
| nextID int | ||
| } | ||
|
|
||
| var ErrNotFound = errors.New("favourite not found") | ||
| var ErrConflict = errors.New("favourite already exists for user and asset") | ||
|
|
||
| func NewInMemoryStore() *InMemoryStore { | ||
| return &InMemoryStore{store: make(map[string][]Favourite)} | ||
| } | ||
|
|
||
| func (s *InMemoryStore) AddFavourite(f Favourite) (Favourite, error) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| // Enforces uniqueness on (userId, assetId). | ||
| for i := range s.store[f.UserID] { | ||
| if s.store[f.UserID][i].AssetID == f.AssetID { | ||
| return Favourite{}, ErrConflict | ||
| } | ||
| } | ||
| f.CreatedAt = time.Now() | ||
| f.UpdatedAt = f.CreatedAt | ||
| s.store[f.UserID] = append(s.store[f.UserID], f) | ||
| return f, nil | ||
| } | ||
|
|
||
| func (s *InMemoryStore) GetFavourites(userID string) ([]Favourite, error) { | ||
| s.mu.RLock() | ||
| defer s.mu.RUnlock() | ||
| return append([]Favourite(nil), s.store[userID]...), nil | ||
| } | ||
|
|
||
| func (s *InMemoryStore) UpdateFavourite(userID, favouriteID string, update Favourite) (Favourite, error) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| list := s.store[userID] | ||
| for i := range list { | ||
| if list[i].ID == favouriteID { | ||
| existing := list[i] | ||
| // if assetId is changing, ensure no other fav uses that assetId for this user | ||
| if existing.AssetID != update.AssetID { | ||
| for j := range list { | ||
| if j != i && list[j].AssetID == update.AssetID { | ||
| return Favourite{}, ErrConflict | ||
| } | ||
| } | ||
| } | ||
| existing.AssetID = update.AssetID | ||
| existing.AssetType = update.AssetType | ||
| existing.Description = update.Description | ||
| existing.AssetData = update.AssetData | ||
| existing.UpdatedAt = time.Now() | ||
| list[i] = existing | ||
| s.store[userID] = list | ||
| return existing, nil | ||
| } | ||
| } | ||
| return Favourite{}, ErrNotFound | ||
| } | ||
|
|
||
| func (s *InMemoryStore) DeleteFavourite(userID, favouriteID string) error { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| list := s.store[userID] | ||
| for i := range list { | ||
| if list[i].ID == favouriteID { | ||
| list[i] = list[len(list)-1] | ||
| list = list[:len(list)-1] | ||
| s.store[userID] = list | ||
| return nil | ||
| } | ||
| } | ||
| return ErrNotFound | ||
| } | ||
|
|
||
| func (s *InMemoryStore) NextFavouriteID() string { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| s.nextID++ | ||
| return fmt.Sprintf("fav-%d", s.nextID) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! 🙏