First you would need to define the scope of the cache (eg: 1. the "time" validity, meaning what's the lifetime of the data you want to cache - will it be destroyed when the user logs out or its session expires and 2. the "spatial" validity of the cache - what are the object types for which the cache will be valid: an user, a group of objects or for the ThingWorx instance etc, 3. the size of that cache and 4. the expected read and write latency of that cache).
A. Properties are meant to store one value (whatever the basetype), but they will store that value in memory, so it will not be useful for large amounts of data. To be clear: for a normal Thing (non-Remote), reading that Property will result in an in-memory read, without a disk read access. Writing can be configured (if you uncheck the Persistent and Logged) to not result in a disk write (but frequently users check the Persistent so that they can get the last value if the Thing or Platform restarts). Latency is very good.
B. User extension properties are good for any user-focused cache (point 2 above) - again, if it matches with your cache definition on top of this reply.
C. Users can use external systems (I include here the standard easily accessible entities like DataTables, Streams or pure SQL tables), but I would not go as far as to consider these systems I mentioned as caches, as writing to them always results in a disk access, hence they won't be suitable for very fast data writes, at least. However, a good third-party system designed for caching will definitely work excellent for this category, just that you need to create an extension - Constantine summarized this aspect.
D. I strongly suggest looking at the OOTB Caching we added for the Mashups - that's really cool - as Rocko started. Starting from that, you can start looking at services like Azure Front Door, that can cache the HTTP requests themselves.
It would be interesting to hear what exact situation you would want to design a cache for, then we can get out of the theorizing mode :D.