Here's what I do. Keep in mind this is just what I came up with, and I'm not claiming it's an optimal solution. It's what works for me. I'm doing procedural generation, so I have to contend with a lot of creation and destruction at run time. If you're not working under the same constraints, there are likely better solutions. But anyway ……..
First I have frame slots. This is information for every frame that can be “in flight”. I support N frame slots but in practice this will be set at like 2 or 3. For every frame slot I have a descriptor heap. Technically you can use one heap for all slots and you can write to it while it's being used by another slot, as long as you don't write to the same area. However, to keep my sanity I decided to make things simple and have one descriptor heap per frame slot (so again like 2 or 3).
I have the concept of a “material”. A material is just a set of shaders and a set of resources that the shaders can use. The resources are what we are concerned with here. I call the resource part of a material a “data-set”. I currently support 0→7 CBVs, 0→7 UAVs and 0→15 SRVs in each data-set. Each data-set is basically a descriptor table in the heap(s).
The tricky part is managing everything. I have a descriptor allocator which allocates descriptor space in powers of two (1,2,4,8,16,32). When you deallocate, it puts space on a free-list for the size you are dealing with, so it can be reused by another data-set later. So for instance, if I need 6 descriptors it would check the size 8 free list to see if there is anything free. If not it adds space to the end of the used area of the descriptor heap. Note in this case there are 2 wasted descriptor slots.
As I said I have a descriptor heap for each frame slot which means most of the time they will be mirrors of each other. However, if you delete a material, it's data-set sill might be in use by an in-flight frame so you can't just destroy everything at once.
What I have is a command queue which takes two types of commands: add data-set and delete data-set. When you want to add a material, you add a command on the queue to do the add of it's data-set. Commands on the queue get executed before each frame. Note, your add command also has to get executed for subsequent frames, so it doesn't get removed from the queue right away. It has a countdown counter. When that reaches zero, we know it's been added to the descriptor heaps for each frame slot, and so we can remove the command from the queue. The same thing happens with delete data-set commands.
One advantage of the separate descriptor heap for each frame slot, is we can grow a heap if it completely runs out of space, and it won't mess up any other inflight frames.
Now we have to deal with the root signatures. My root signatures have a couple slots for general purpose constant data that's used in all rendering. The final slot is a pointer into to the table for the material's data-set we are currently using. The tricky part is, that slot has to be configured for the number of CBVs, UAVs and SRVs in the material we are rendering with. This means I would normally have to create and destroy root signatures a lot, which I wanted to avoid.
What I have is root signature pool. Since the last one (SRVs) can be variable all I have to worry about is variations in the CBV count and UAV count, which is 8 x 8 or 64. So I have a table of 64 possible root signatures (Edit: Actually, I think it's 128 to handle the case where there are no SRVs at all) . They aren't actually all created, however. Each root signature is created the first time it is needed and then cached for future use, so in general most of the pool will be empty.
This is all from memory, so I hope I didn't miss anything. As you can see this stuff can get a bit sophisticated, but again you may not need all the features I do, or you may need different features.