Skip to content

feat: implementation of favorites api#73

Open
mbadjohn wants to merge 16 commits into
GlobalWebIndex:mainfrom
mbadjohn:feat-interview-task-mkakogiannou
Open

feat: implementation of favorites api#73
mbadjohn wants to merge 16 commits into
GlobalWebIndex:mainfrom
mbadjohn:feat-interview-task-mkakogiannou

Conversation

@mbadjohn

Copy link
Copy Markdown

Summary

Implements the GWI Favourites API in Go: list (paginated), add (full body or by catalog source_asset_id), remove, and update description for favourite assets (charts, insights, audiences).

  • Architecture: Hexagonal (favourites + assets hexagons), OpenAPI/Swagger + oapi-codegen, JWT auth
  • Storage: In-memory store and catalog with seed; add-by-reference validates against catalog
  • Consistency: Asset-deleted event removes matching favourites
  • Validation: go-playground/validator; tests for repo, service, and HTTP
  • Run: go run ./cmd/favourites-api · Tests: go test ./...

@mbadjohn mbadjohn changed the title Feat interview task mkakogiannou feat: implementation of favorites api Feb 12, 2026
@mbadjohn mbadjohn marked this pull request as ready for review February 14, 2026 15:04

@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 @mbadjohn 👋

Thank you very much for taking the time to complete the assignment!
Nice touch with the openapi code generation 👌!

I've left some comments for you to have a look when you have the time

Comment thread http/server.go
Comment on lines +190 to +191
asset := apiAddRequestToDomain(req)
added, err := s.UC.AddFavourite(string(userID), asset)

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 happens if a user provides an input which has all chart, insight and audience filled in the request? How is this currently handled and would you change anything?

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.

Yeap, absolutely. that's a good catch actually. if a user provides multiple asset type fields (e.g., both chart and insight), the validation doesn't reject it, by returning the respective message. it would copy all provided fields into the domain Asset. This could lead to semantically incorrect data where a chart type asset has insight data attached.

I would reject the request most probably. Thus, meaning that I would add an extra validation to ensure only the matching field is provided, I would return a 400 - Bad Request if extras were detected.

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 please elaborate on what are the differences between this implementation and the one declared in assets/domain/asset.go?

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.

These are intentionally separate bounded contexts, not duplicated models and each serves a different purpose.

assets/domain is the company's master catalog. Each type (Chart, Insight, Audience) has its own struct on a shared BaseAsset. Fields like Gender []string are arrays because the data is genuinely multi-valued, and OwnerID handles catalog-level access control.

favourites/domain is the user's personal collection. A single Asset struct trimmed down for storage and serialization. Ownership is implicit in the repo key, SourceAssetID links back to the catalog, and users can override Description with their own notes.

The adapter in favourites/adapters/assetcatalog/client.go bridges the two, so each context can evolve independently.

Bonus: if catalog ever becomes its own microservice, only the adapter changes, domain and application layers stay untouched.

AssetTypeAudience = "audience"
)

type Asset struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lets say that you wanted to add a DB into the application? What would that be and how would the schema look like?

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.

In production I'd go with PostgreSQL using a hybrid approach. Core queryable fields like user_id, type, description and timestamps would be proper relational columns, while the polymorphic type-specific data would live in separate JSONB columns (chart_data, insight_data, audience_data). A CHECK constraint would ensure only one of those columns is populated at a time, which enforces the same discriminated union contract we handle at the validation layer.
For indexing, a composite (user_id, created_at DESC) covers the main access pattern of listing a user's favourites chronologically and makes keyset pagination efficient at any scale. A partial index on source_asset_id (WHERE NOT NULL) keeps the RemoveFavouritesBySourceAssetID operation snappy for the asset deletion handler.
I'd also add a FOREIGN KEY on source_asset_id referencing the assets catalog with ON DELETE CASCADE. That way referential integrity lives at the database level and when a catalog asset is deleted, favourites get cleaned up automatically, which would make the event-based logic in OnAssetDeleted() redundant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hello @mbadjohn, could you explain your decision on introducing two separated in-memory stores one for the assets and one the favorites? What are the benefits of that? Could you think of any cons?
Also, could you explain how your implementation supports concurrent actions and ensures atomicity?

@mbadjohn mbadjohn Mar 1, 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.

The main reason is that they serve fundamentally different purposes. The asset catalog is the platform's master data, shared and read-only, while favourites are user specific and constantly changing. Keeping them separate means each can evolve on its own, the catalog stays stable and cacheable (Redis in production), while favourites can be optimised for writes (PostgreSQL). It also supports both "add by reference" (starring a catalog item) and "add by value" (creating a custom favourite).
The honest downsides are more complexity, some data duplication since favourites store full asset data, needing event handling to clean up orphans when catalog items are deleted, and no distributed transactions across the two stores.

Now on concurrency point, both stores use sync.RWMutex. Read operations use RLock so multiple readers can run concurrently, while writes take a full Lock for exclusive access. This gives decent read throughput without sacrificing write safety.

On atomicity, each repository method is atomic on its own, for example Add updates both the users map and sourceAssetIndex under a single lock. Cross-store operations like a catalog lookup followed by a repo add aren't atomic, but that's acceptable since the catalog is read-only. Where it gets trickier is at the service level, where multiple repo calls in one use case are independent transactions with no coordination between them.
For production with stricter requirements I'd look at optimistic locking, event sourcing, or the SAGA pattern for distributed transactions.

Comment thread assets/domain/asset.go
)

// BaseAsset is the shared identity and metadata for every catalog asset.
type BaseAsset struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could you propose an alternative approach for structuring your Asset model? Instead of embedding is there any other concept that could be used here?

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.

An alternative would be interface based polymorphism. This would allow storing mixed types in a single collection and writing generic functions that operate on any asset type.

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