You'd ideally want to do something like dataloader, where you look up your N Xs in a single cache query, and then do a single database lookup for the (N-C) Xs that weren't in cache. You can then either eagerly load the Ys with the Xs like you said, or do a secondary cache lookup for every Y, and potentially another single database query for the Ys not in cache.
Unfortunately this pattern gets really hairy if you're not using promises and an event loop.
If you have multiple ways to 'see' the same X from multiple Y objects, then all of this get complicated quickly.
Once you're there a microservice has some advantages. Wrap a cache with a service, implement multi-get, anything not in the cache calls through to the database.
+1. The JS event loop auto-monad-izing Promises into Haxl [1]-esqe trees of implicitly-batched loads has been a big win for us building on JavaScript/TypeScript.
If I had to move to another language, I'd really want to find a "powered by the event loop / dataloader" framework, i.e. Vert.x for Java.
Also, per dataloader, a shameless plug for our ORM that has dataloader de-N+1-ing built natively into all object graph traversals:
Indeed, the parent was describing built-in features of the Apollo GraphQL Client, such as entity-level caching in the frontend.
However, a dataloader pattern can also be useful when implementing custom resolvers for a GraphQL API. For this purpose, Apollo provides data sources which handle caching at request level.
Unfortunately this pattern gets really hairy if you're not using promises and an event loop.
https://www.npmjs.com/package/dataloader