GWI Go Platform Challenge Submission#56
Conversation
NikosMas
left a comment
There was a problem hiding this comment.
Hello Mary 👋
Thank you for your time and for the amount of work you’ve done!
I really liked several aspects of your approach on the project:
- The overall project structure and packaging
- The error handling using common error types
- The extensive test coverage you added
- The way you handled reads/writes in the storage layer (dictionaries) with mutexes
- The fact that you added an authorisation flow
I’ve left some comments, and I also have a few questions:
- You mentioned that you would use PostgreSQL if you chose to persist the data. Why PostgreSQL and not a NoSQL database?
- Regarding logging, where would you add logs and why?
- Suppose I add a favourite asset and then cancel the request almost immediately. How do you (or how would you) handle such cases—e.g., ensuring consistency and dealing with partial or cancelled operations?
Thanks again!
|
|
||
| userFavorites, exists := fs.favorites[userID] | ||
| if !exists { | ||
| return errors.New("user has no favorites") |
There was a problem hiding this comment.
This error will lead to a 500 error response to the user. Same error here in the RemoveFavourite method returns 404 error response. Which approach works better for you?
General question:
When would you prefer using the 500 and when the 4** error responses?
There was a problem hiding this comment.
Let me start by mentioning that if there was anything that I would have liked to spend more time on it would be error handling; admittedly it has been inconsistent and this is one example that escaped my proofreading. Ideally I would create a method with a switch statement that would consistently return a certain error code according to the error type.
Now to answer your question: I would use the 500 error code for server-side errors (unexpected failures like logic bugs, database crashes, unhandled exceptions etc.) and 4xx codes for client-side errors (bad request, unauthorized etc.). In retrospect I would have changed this to return ErrAssetNotFound like in RemoveFavorite and the handler to respond with 404.
| defer userLock.Unlock() | ||
|
|
||
| if _, exists := fs.favorites[userID][favorite.GetID()]; exists { | ||
| return errors.New("asset already in favorites") |
There was a problem hiding this comment.
This error here will lead to a 500 error response to the user. Is there any other status code that could be used for this kind of error?
There was a problem hiding this comment.
Yes, in retrospect I would return a 409 error response; the client is trying to create a resource that already exists, which conflicts with the current state.
| http.Error(w, "Failed to encode error response", http.StatusInternalServerError) | ||
| } | ||
| return | ||
| } |
There was a problem hiding this comment.
Lines 36 to 49 are duplicated for every method in the asset handler. Also, this pattern, with the error mapping to HTTP errors, is used in all handlers. Can you think of a better approach to avoid the duplication?
There was a problem hiding this comment.
As an immediate improvement, I would use a method that takes the error code as input and responds appropriately. It would look like this:
func WriteJSONError(w http.ResponseWriter, message string) {
statusCode := MapErrorToStatusCode(message)
errorResponse := ErrorResponse{Error: message}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(errorResponse); err != nil {
http.Error(w, "Failed to encode error response", http.StatusInternalServerError)
}
}
MapErrorToStatusCode would essentially be a bit switch statement that would map all the errors defined in errors.go to an http status code.
For a more long-term approach that is slightly more verbose but also type-safe is to have typed errors that wrap around the error type, which would look like:
type AppError struct {
Code string
Message string
StatusCode int
Err error // underlying error for wrapping
}
and an example usage would be:
var (
ErrInvalidUserID = &AppError{
Code: "INVALID_USER_ID",
Message: "invalid user id",
StatusCode: http.StatusBadRequest,
}
ErrUserNotFound = &AppError{
Code: "USER_NOT_FOUND",
Message: "user not found",
StatusCode: http.StatusNotFound,
}
)
WriteJSONError would also be simpler, not to mention we would be avoiding a giant switch statement.
| ServerPort string | ||
| TokenExpiry time.Duration | ||
| ContextKeys struct { | ||
| UserID ctxKey |
There was a problem hiding this comment.
What's the difference in using a string type?
There was a problem hiding this comment.
I opted to use custom unexported types instead of string because it is recommended by the context package docs since they provide:
- Collision avoidance: If I used plain string, any package could use the same "userID" string and create collisions. With my chosen approach even if another package defines
type ctxKey stringwith the same constants, Go's type system would treat them as different types. - Encapsulation:
ctxKeyis unexported, so external packages can't create their ownctxKeyvalues; they must useUserIDandRole.
| userID := uuid.New() | ||
| numAssets := 50 | ||
|
|
||
| var wg sync.WaitGroup |
There was a problem hiding this comment.
Are there any other types of groups, and why did you choose the waitgroup?
There was a problem hiding this comment.
I had to do a bit of research to provide a more complete response. There are definitely other types of groups like channels, errgroup.Group, errgroup.WithContext, context with channels, and semaphores. Most - if not all - of the ones I just mentioned support error handling and cancellation. Both sync.WaitGroup and errgroup.Group/errgroup.WithContext are low-complexity options, with errgroup.* having the error handling and cancellation advantage over sync.WaitGroup. However, I still opted for sync.WaitGroup because:
- We just need to wait for goroutines to finish.
- There are no errors to collect (test failures are handled by
t.Errorf). - There is no need for cancellation in a unit test.
- It is part of the standard library, so there are zero dependencies.
I would use the alternatives in the following scenarios:
- Production code with errors:
errgroup.Grouporerrgroup.WithContext - Need for cancellation:
context+ channels orerrgroup.WithContext - Rate limiting:
semaphore.Weighted - Result collection: channels
ba091724
left a comment
There was a problem hiding this comment.
Hello @Mary-LP,
Thank you for taking the time to design, implement, and nicely document the assignment!
I have added a couple of comments for you to look when you have the time.
I would also like to get your opinion regarding the following scenario:
Let's consider a case where the application database hosts a large amount of assets, persisted under an entity table named Assets. How would you adapt your implementation to support/enhance the functionality of user favorites? Would you change the way user favorites are handled (created/updated/deleted)?
|
|
||
| // Return the JWT token to the client | ||
| w.Header().Set("Content-Type", "application/json") | ||
| if err := json.NewEncoder(w).Encode(map[string]string{"token": token}); err != nil { |
There was a problem hiding this comment.
How would you handle a real-world application where the web client (i.e. the company website) needs to receive the JWT?
There was a problem hiding this comment.
To be fully honest, I personally do not have a lot of exposure to the authentication side of applications so I had to do some quick research to answer this. The method I narrowed it down to - assuming the company website is an SPA - is using HTTP-only cookies; they cannot be accessed via JavaScript and they help protect against XSS attacks. I would also enhance this with a refresh token - also stored in an HTTP-only cookie, separate from the JWT - to be used to request a new JWT when the previous one expires.
At the end of this evaluation process I would appreciate it if you could provide your own thoughts and/or preferred methods.
There was a problem hiding this comment.
Your research is valid, yes. Personally, and considering the scope of the assignment, I think it suffices to mention that including the JWT in the response body is a simplicity-driven decision and not an option in a real-world application. IMHO the Set-Cookie response header is ok for this context.
| api.HandleFunc("/favorites/list", favoritesHandler.list).Methods("GET") | ||
| api.HandleFunc("/favorites/add", favoritesHandler.add).Methods("POST") | ||
| api.HandleFunc("/favorites/remove", favoritesHandler.remove).Methods("DELETE") | ||
| api.HandleFunc("/favorites/update", favoritesHandler.update).Methods("PATCH") |
There was a problem hiding this comment.
Can you spot one or more weak points regarding this endpoint (API design, implementation)?
There was a problem hiding this comment.
I can spot a few issues:
- All the endpoints are slightly verbose and are inconsistent with REST conventions, where the ID should be in the URL.
- They are harder to cache/log/audit since the IDs are in the query string.
- No
OPTIONSorHEADsupport, which may break CORS - No rate limiting; DDoS vulnerability
- No request size limits; huge JSON payloads could exhaust memory
- No logging or tracing; harder to debug production issues
As a step-one improvement, I would rework these lines as follows:
api.HandleFunc("/favorites", favoritesHandler.list).Methods("GET") // List
api.HandleFunc("/favorites", favoritesHandler.add).Methods("POST") // Add
api.HandleFunc("/favorites/{id}", favoritesHandler.update).Methods("PATCH") // Update
api.HandleFunc("/favorites/{id}", favoritesHandler.remove).Methods("DELETE") // Delete
Adding OPTIONS or HEAD support, rate and size limiting, and logging/tracing is another can of worms I cannot reasonably address in a comment.
|
Hi @NikosMas, Thank you for your comments and thought-provoking questions. I hope my answers to your comments are sufficient and, if not, please do not hesitate to follow up. As for your questions:
Purely for familiarity reasons; I have used PostgreSQL in the past and would know how to handle and set it up with a decent amount of reliability. I would not be opposed to a NoSQL database and it may indeed a better solution, but I would need more time for research.
There are plenty of places I would (and should) have added logs. Some of them are:
What I would ideally wish to do is not the current behavior:
|
|
Hi @ba091724, Thank you very much for your comments. I hope I have answered them sufficiently and, if not, please do not hesitate to follow up. Especially for your first one I will admit again some unfamiliarity with authentication and token handling. Now for your question:
As I mentioned to Nikos, I am more familiar with relational databases and approaches, so my answer is going to be geared around that. I would approach this case as follows:
To briefly elaborate on the asset handling assuming the concept of asset ownership that I mentioned above:
I hope this answers your question. Please let me know if you have any further comments or questions! |
Dear GWI team,
This PR contains my submission for the GWI Platform Go Challenge.
README.md.Thank you for reviewing! Please do not hesitate to contact me with comments, concerns or requests for alterations.
Kind regards,
Mary Loukidi-Papanikoli
P.S. I opted to do the development privately and only push the completed project. This is the exception, not the rule.