feat: implementation of favorites api#73
Conversation
| asset := apiAddRequestToDomain(req) | ||
| added, err := s.UC.AddFavourite(string(userID), asset) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Can you please elaborate on what are the differences between this implementation and the one declared in assets/domain/asset.go?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Lets say that you wanted to add a DB into the application? What would that be and how would the schema look like?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| ) | ||
|
|
||
| // BaseAsset is the shared identity and metadata for every catalog asset. | ||
| type BaseAsset struct { |
There was a problem hiding this comment.
Could you propose an alternative approach for structuring your Asset model? Instead of embedding is there any other concept that could be used here?
There was a problem hiding this comment.
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.
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).go run ./cmd/favourites-api· Tests:go test ./...