Skip to content

Implement Favorites API with validation, tests and Docker support#69

Open
kongrillas wants to merge 4 commits into
GlobalWebIndex:mainfrom
kongrillas:feature/interview-task
Open

Implement Favorites API with validation, tests and Docker support#69
kongrillas wants to merge 4 commits into
GlobalWebIndex:mainfrom
kongrillas:feature/interview-task

Conversation

@kongrillas

Copy link
Copy Markdown

Summary

This PR implements a lightweight HTTP API that allows users to manage a per-user list of favorite assets.

The solution focuses on correctness, clarity, and pragmatic design choices, while keeping dependencies minimal and the codebase easy to reason about.


Features

  • REST API for managing user favorites (list, add, update description, remove)
  • Support for multiple asset types:
    • Insight
    • Chart
    • Audience
  • Type-specific payload validation per asset type
  • Concurrency-safe in-memory storage using RWMutex
  • Handler-level tests using httptest
  • Multi-stage Dockerfile for containerized execution

Design Notes

  • No external routing or validation libraries were used to keep dependencies minimal.
  • Asset payloads are validated via type-specific structs instead of rigid schemas, allowing flexibility for future extensions.
  • In-memory storage was chosen for simplicity; the design allows easy replacement with persistent storage.

@dzacharakis dzacharakis left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kongrillas 👋

Thanks for your work on this assignment!

I added some comments on your PR with a few questions. I’d love to hear your feedback when you get a chance.

Thanks,
Dimitris

Comment thread internal/model/asset.go
ID string `json:"id"`
Type AssetType `json:"type"`
Description string `json:"description,omitempty"`
Payload json.RawMessage `json:"payload"` // type-specific

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re using json.RawMessage for this polymorphic field. What are the drawbacks of this approach?
Can you suggest a better alternative?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using json.RawMessage allows deferring payload decoding until the asset type is known, which keeps the initial model flexible and helps to avoid "premature" coupling. So, for this task, I chose RawMessage, in order to keep the design as simple and flexible is possible, but having correct approach though every validation per asset type.

The main drawbacks, I can think of are:

  1. Loss of compile-time type safety for the payload field.
  2. Validation that will rely on runtime checks and will happen later than it should.
  3. Making refactoring weaker.

As better alternative, I can think to model the payload as an interface and use a custom UnmarshalJSON, based on the asset type.

Comment thread cmd/server/main.go
Comment on lines +7 to +9
"github.com/kongrillas/platform-go-challenge/internal/http/handlers"
"github.com/kongrillas/platform-go-challenge/internal/http/router"
"github.com/kongrillas/platform-go-challenge/internal/store"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose internal? Is it because you are following a standard project/directory layout or other reason?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the internal layout to clearly communicate package boundaries and prevent any unneeded reuse of application internals outside the module. Because in my example, HTTP handlers, models and storage are implementation details of the service and I do not want to be used by external consumers.

Comment thread cmd/server/main.go
app := router.New(h)

fmt.Println("listening on :8080")
_ = http.ListenAndServe(":8080", app)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the choice of using the blank identifier here?
The error is intentionally ignored/ silently discarded—what makes that the right decision, and is there a better alternative?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blank identifier was used to keep the example minimal and focused on the API logic. Of course, I get it is not ideal, because in a production system, we should have graceful shutdown, context cancellation and a proper error propagation. As better approach I would recommend something that logs the error and exit the process or wrap ListenAndServer like that:

log.Fatal(http.ListenAndServe(":8080", app))

)

type FavoritesHandler struct {
Store *store.MemoryStore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are possible drawbacks of using a concrete *store.MemoryStore?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, like that we reduce flexibility and testability.

The main drawbacks are:

  1. It is harder to swap storage implementations (for example: database-backed store)
  2. Tighter coupling between HTTP layer and persistence
  3. Mocking requires concrete types instead of interfaces

A more flexible approach would be to depend on a Store interface that defines the required behavior, allowing different implementations (in-memory, database, cache-backed) without changing the handler logic.

For this exercise, I chose the concrete type for simplicity and clarity, but introducing an interface would be a natural next step.

func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen here if JSON encoding fails?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If JSON encoding fails, the error is currently ignored and the response may be partially written or invalid. Of course, this cannot be used in a production system where a more suitable approach would be to log the error, avoid writing the body, when the headers are not sent yet and return an internal server error when it's possible.

In a real-world service proper error handling would be required, I used this approach here assuming encoding failures are unlikely given validated input.

"strings"
)

var ErrInvalidPayload = errors.New("invalid payload")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the downsides of returning the same error for every validation failure?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning a single generic validation error simplifies the API but reduces observability and the feedback that you can receive from the client or anyone, who uses it. So, it is harder to debug, you lose field-level error information and it's difficult to distinguish different failure causes.

I chose the simpler way for this challenge, to keep the validation logic straght-forward and in general to avoid over-engineering to the whole project.

Comment on lines +82 to +83
_ = rr // silence unused if kept
_ = req

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you used a linter, and did you explicitly silence the type-check warnings to handle those cases?
Here it doesn’t seem necessary (wouldn’t it be more appropriate to silence line 68 NewRecorder()?),
but do you know of another way to handle linter issues?

@kongrillas kongrillas Feb 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn’t run a linter explicitly for this task, but I’m familiar with common Go linters such as golangci-lint and staticcheck.
In this case, the blank assignments were used to silence unused variables introduced for test clarity.

Alternative approaches could include:

  1. To restruct test to avoid unused variables
  2. To limit the variable scope
  3. To silence warnings closer to the actual source (as you suggested)

In production code, I would prefer restructuring over silencing wherever possible.

@pamanta pamanta left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kongrillas,

Thank you very much for taking the time to complete the assignment.

I have also added a couple of comments for you to take a look when you have the time

Comment on lines +22 to +35
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.ListFavorites(w, r)
case http.MethodPost:
h.AddFavorite(w, r)
case http.MethodDelete:
h.RemoveFavorite(w, r)
case http.MethodPatch:
h.UpdateDescription(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you identify any drawbacks or issues this routing mechanism might have? If yes, how would you fix them?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current routing is intentionally minimal (stdlib ServeMux + switch r.Method), but I can see some drawbacks:

  1. ServeMux doesn’t provide first-class path params, so parsing {userId} / {assetId} becomes manual and error-prone as routes grow.

  2. Switch per handler scales poorly as endpoints expand, and it’s easy to miss consistent behavior.

  3. Adding auth, logging, request IDs, etc. becomes repetitive without a router chain.

  4. As the API grows, it’s harder to understand the routing table and enforce consistent patterns.

In a production setting, I’d likely use a lightweight router (e.g. chi) to get clean path params, method routing, and middleware chaining out of the box. In both cases I’d add consistent error handling + structured logging + request-scoped context.

Comment thread internal/model/asset.go
@@ -0,0 +1,18 @@
package model

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you were to add persistence, which type of database would you use, why would you choose it, and how would you model the schema?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For persistence I’d likely choose PostgreSQL, for its strong consistency and because it fits to favorite lookups. Also it's easy to model uniqueness (no duplicates per user+asset), indexing, and transactional updates.

For polymorphic payload, Postgres JSONB is a pragmatic middle ground: flexible schema + queryable if needed.

For a heavy production project:

favorites(user_id, asset_id, type, description, created_at, updated_at)

type-specific tables:

favorite_insights(asset_id, text, …)

favorite_charts(asset_id, title, axes, data, …)

favorite_audiences(asset_id, …)

For this task I used an in-memory store for simplicity, but Postgres + JSONB with a composite key and proper indexing would be my default production choice.

@dzacharakis

Copy link
Copy Markdown

Thank you Konstantinos for your responses to our comments.
All good on my end. Good luck with the next steps!

@kongrillas

Copy link
Copy Markdown
Author

Thank you Konstantinos for your responses to our comments. All good on my end. Good luck with the next steps!

Thanks a lot too Dimitris, looking forward for the next steps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants