With my system, each capability type that needs to be synced to the client has several sync-related classes:
A single update network message (extending UpdateContainerCapabilityMessage) that syncs the capability data for a single slot of a Container.
A bulk update network message (extending BulkUpdateContainerCapabilityMessage) that syncs the capability data for all slots of a Container.
A "functions" class containing static methods used by both the single and bulk update messages.
A container listener class (extending CapabilityContainerListener) that sends the single/bulk update messages when the Container's contents change.
A factory function for the container listener is registered with CapabilityContainerListenerManager.registerListenerFactory at startup so that when a player opens a Container, a new listener can be created and added to it.
The network messages have the concept of a "data" class, which is a simple POJO (or even a primitive type like int or long) containing only the data that needs to be synced from the server to the client. The base classes for the messages handle the functionality that's common to all capability types, the message classes for each capability just need to provide functions to do the following:
On the server:
Convert an instance of the capability handler (e.g. IFluidHandlerItem for a fluid tank) to a data object
Encode (write) the data object to the packet buffer
On the client:
Decode (read) the data object from the packet buffer
Apply the data from the data object to the capability handler instance
These functions could be defined anywhere (they could even be lambdas passed directly to the base class methods), but I keep them as static methods in a "functions" class so they can be shared between the single and bulk messages.
The system might be a bit over-engineered, but it means that I can easily add syncing for a new item capability without having to rewrite all the common syncing logic.
There are several implementations of this in TestMod3 that you could use as examples:
ILastUseTime, which tracks the time at which an item was last used. This is a simple implementation that syncs a single long value to the client, so it uses Long as its data class.
Single update message
Bulk update message
Functions class
Container listener
Container listener registration (called during FMLCommonSetupEvent)
IFluidHandlerItem, Forge's fluid tank capability. This is a slightly more complex implementation that syncs a FluidStack (the tank contents) and an int (the tank capacity), so it uses FluidTankSnapshot as its data class.
Single update message
Bulk update message
Functions class
Container listener (only syncs data for my own fluid tank item, to avoid conflicts with other mods' fluid handler items)
Container listener registration (called during FMLCommonSetupEvent)