Since your service is read-intensive, using a caching proxy sounds like a good idea. But beware: the more you try to retain the semantics of the original system when the backend is down, the more complex your system becomes. And usually, the more surprising its failure modes are to the end-user. Both of those factors will often motivate a decision to adopt a simple read-only proxy.
Even a read-only proxy should return a timestamp with the data, to indicate when the data was last fresh. For HTTP you can encode this with IMS or an Etag; for other systems it will depend on the protocol you are using. Refusing to serve very unfresh data from the proxy is a choice you might but don't have to make.
In your application layer, you will need to decide what to do when the user wants to perform a mutation-like action on data of age T seconds.
I think it is normally best to just accept the change, and return an error if the (Cool.io) backend failed to process the request. If you choose instead to decline without trying, you will have problems in which the pre-mutation check finds the backend in one state, but the attempt to actually apply the change finds it in another -- this situation is hard to test for in your system's regression test suite, so my advice is not to build a system that tries to do this.
If the backend cannot apply your change, your system could treat this error as final or it could offer to try to apply the change when the service returns.
As a user-experience optimisation, when you know that the proxy has been unable to get service from the backend, you can display warnings on the user interface, so that they user can avoid a time-consuming data entry only to find the backend is down.
If you do offer to apply a failed change later, the changes the user wanted to apply but which could not be applied in the short term are going to need to be stored. You can store them in a queue, but as @matthew-flynn pointed out, you will need to handle duplicate (perhaps conflicting) queued changes. Hence you will probably need to "queue" the changes in a queryable way. Such as in a database table of unreconciled changes. The simple thing to do there is to reject changes to data which itself hasn't really been applied to the backend. Otherwise, a failure to apply a certain change to the backend may require more than one user-level change to be rejected.
If it is possible for a queued change to fail to apply you are going to need to provide some kind of functionality in which the user reconciles the failed changes.
A particularly interesting case is where the backend has just come up and another user has submitted a conflicting change, live. That is, a new change has "overtaken" a queued change. You might consider blocking all changes by users when there are queued pending changes. One way to achieve that is for all changes, live or deferred, to use the same queue. If you do that, be very sure that problem changes cannot get stuck at the head of the queue.
As you will note from the above, all this this clearly requries changes in the semantics of the application. You can't just do it invisibly in a proxy layer, unless you reject mutations that couldn't immediately be applied to the backend. And if it makes a difference to the user whether the data is fresh or not you may need to warn them that they're looking at stale data.
You also asked how large services deal with this. One of the popular ways is for the backends to be sharded by user-id so that if a given part of the service is down, only some users are affected. This is easy to do for things like serving static data (which mostly won't care who you are) but much harder for services in which users have N-to-N relationships (for example things like Twitter - though in the case of Twitter the complexities around failed mutations are mostly absent).