Encrypting and decrypting strings using Caesar cipher
Docstrings:
If using (and only targeting) python 3.5 or later, you could remove the type hints from the docstring and include them as real type hints.
You could also extend the docstrings to contain a (doctest-conforming) example usage.

Algorithm:
Unicode is HARD. Python3 strings are unicode. The caesar cipher only works for letters that are in ascii range. Example:

&gt;&gt;&gt; rotated_letter("Á", 13)
'L'
&gt;&gt;&gt; rotated_letter("L", -13)
'Y'


So your test by isupper / islower is not enough. You could do 'a' &lt;= letter &lt;= 'z' for an ascii lowercase test.

Variable names:
Just a gut feeling: functions do stuff. So a good name for a function is an imperative: I'd use rotate_letter instead of rotated_letter.
Also, once you name the functions rotate_xxx, the concept of a key is not valid anymore (this belongs into functions like encrypt and decrypt). When speaking of rotation, I'd call the argument offset or something like that.

Memoization / Performance:
The letter encryption function does not do much, so memoization is probably not necessary. 
You are concatenating strings for building the result. This is slow. If this slowness doesn't matter to you (because of your small-input use case), then memoization is overkill. If you want better performance, you would (in the general case) append your string parts onto a list, and join the list when the calculation is done. In this special case, I would even opt for a preallocated array, as the output size is known.

PS:
I learned something, too, i.e. that there is setdefault() for dicts. Thanks!