Cache Notifications and Event-driven Architecture with NCache

Introduction

Imagine you are searching for a product on an online shopping website. You find the product and see that the item is in stock. You have added the item to your cart and while trying to checkout, you instantly see that the product has suddenly gone out of stock.

On another website, you are trying to buy a product and you see the website shows you x number of users have purchased this product in the past y days and there's a live counter that shows how much of stock is left which goes down as time goes by.

These are some examples of applications leveraging the power of event-driven architecture.

In this architecture, a fleet of microservices access data from the backend via a distributed cache layer for high performance. Whenever data is modified in the cache, these microservices are notified and the change is immediately propagated among all the microservices. This design helps in building applications that are scalable and can stay updated with data modifications that happen in the backend.

Understanding Caching and Event-driven Architecture

Caching plays an important role in optimizing application performance by storing frequently used data in memory.

This reduces the need to retrieve data from slower data sources like databases. On the other hand, event-driven architecture decouples components of a system, allowing them to communicate through events, enabling real-time responsiveness and scalability.

Benefits of Cache Notifications in an Event-driven Architecture

  • Real-time Data Synchronization - When cached data changes, relevant components of the system can be instantly notified, ensuring that all instances are working with the most up-to-date information.
  • Scalability and Performance - By reducing the load on databases and enhancing data retrieval from cache, systems can maintain optimal performance and scale more effectively.
  • Eventual Consistency - With cache notifications, the data in the cache remains consistent with the underlying data source, ensuring that all components are in sync.
  • Reduced Latency - Event-driven communication minimizes latency since components react promptly to changes instead of polling for updates.

Five key considerations while building application experiences with Cache Events

While the combination of cache notifications and event-driven architecture offers numerous advantages, it's important to address certain key considerations.

  1. Data Consistency and Synchronization - Ensuring data consistency across distributed components is critical. Changes in one cache instance may impact other related caches or services. We need to implement proper synchronization mechanisms to prevent data inconsistencies and ensure that all subscribers receive accurate updates.
  2. Notification Granularity - Broadcasting notifications for every small change can lead to excessive network traffic and unnecessary processing. It is highly important to balance between notifying about meaningful changes while avoiding overloading the system with notifications.
  3. Error Handling and Resilience - We need to ensure that our cache notification system is resilient to network disruptions, temporary outages, or service failures. Implementing proper error-handling strategies to gracefully recover from failures and ensuring that notifications are not lost is highly recommended.
  4. Scalability and Performance - As the system grows, the volume of cache notifications can increase significantly. We must consider how the cache notification mechanism scales. Load balancing, partitioning, and distributed architectures can help manage the increased load. Optimize the performance of the cache notification mechanism to avoid any unexpected bottlenecks.
  5. Eventual Consistency vs. Immediate Consistency - Cache notifications often provide eventual consistency. It means that updates may not be immediately propagated to all subscribers due to network delays or system failures. It is necessary to evaluate whether immediate consistency is crucial for the use case or if eventual consistency is acceptable. Implement strategies to mitigate any potential user experience issues resulting from this delay.

Cache Providers which support Cache Event Notifications

Several cache providers in the market offer support for cache notifications, enabling real-time updates within event-driven architectures. Some notable cache providers with cache notification capabilities include Redis, Memcached, Hazelcast, Apache Ignite, and NCache. In this article, let us deep dive into the Cache Notifications feature and options supported in NCache.

What is NCache

NCache is a distributed caching solution that provides a highly scalable and reliable in-memory data store. Its capabilities include cache synchronization, distributed caching, and cache notifications.

Working with Cache Event Notifications with NCache

We can classify NCache event notifications into 3 types based on the type of information we can subscribe to -

  1. Data Specific Events
    1. Cache Level Events
    2. Item Level Events
  2. Management Level Events
  3. Client Activity Events

Data Specific Events

We trigger Data Specific Events when we change data in the cache by adding, updating, or removing it. These events also happen when we add, update, or remove data in the cache due to Cache Startup Loader/Refresher and Backing Source activities.

To get notifications for these events, we use the MessagingService.RegisterCacheNotification method. We can stop getting notifications for specific data-related events by using the MessagingService.UnRegisterCacheNotification method.

Data Specific Events can be further classified into two types based on the scope level -

Cache Level Events

We create Cache Level Events as general events for the cache. We trigger them when we perform Add, Update, or Remove operations on cache data. These events let different clients know about each action happening in the cache. However, we should be careful with Cache Level Events because if we register them for every operation on all data in the cache, our application's performance might have an impact.

By default, Cache Level Events are turned off for any cache setup (except for the cache cleared operation). To make these events work, we need to enable Cache Level Events using the NCache Manager.

Item Level Events

Item Level Events are useful when we only want to know about certain data changes, not all changes in the cache. We can set up notifications for just the data we need to be notified about. The cache will track that particular set of keys for changes. We set this up by using API calls to register a callback method for a specific data key in the cache. When that key changes, we get a heads-up.

These notifications are sent to the client asynchronously. Item Level Events only work for changes to data already present in the cache. We won't get a notification when something new is added.

Management Level Events

We use Management Level Events to get notifications when any management operation happens in the cache cluster, using the NotificationService interface. These events are useful for checking if operations should keep going in the cache or not, particularly in cases such as preseeding a cache with data by a job or a service that runs in a separate thread. We can subscribe for an event that is returned when the cache stops.

The following event notifications are supported -

  1. Cache is cleared
  2. Cache is stopped
  3. New member joins the cluster
  4. An existing member leaves the cluster

Client Activity Events

Client activity events tell us when clients connect or disconnect. When a client joins a clustered cache, it can subscribe to these events. It can then know when other clients connect or disconnect. We can even pick how long a disconnected client is thought to be disconnected before we get a notice. If a client reconnects within that time, we won't get a notice.

This helps when a client accidentally disconnects from the network. NCache's client figures this out and reconnects automatically. Keep in mind that this doesn't work for clients that decide to close and restart themselves. We need to turn on client activity notifications in NCache Manager for this feature to work.

Example Implementation of Cache Level and Item Level Event Notifications with NCache and .NET

The following code snippet shows a sample implementation of BookStoreNotificationService that subscribes to both cache-level and item-level events in NCache.

Keep in mind that the cache key being used to subscribe for notifications must exist in cache. Also, you can only subscribe to the update or remove event types for Item Level Events. You must enable event notifications using NCache Manager for these to work.

/// <summary>
/// Book Store Notification Service shows how to implement
/// Cache-level event notifications
/// </summary>
public class BookStoreNotificationService
{
    // event descriptor stores subscription for all cache events
    private readonly CacheEventDescriptor _cacheLevelEventsDescriptor;
    private readonly ICache _cache;


    public BookStoreNotificationService(string cacheName)
    {
        _cache = CacheManager.GetCache(cacheName);
        _cacheLevelEventsDescriptor = RegisterCacheNotificationsForAllOperations();
    }


    public void OnCacheDataModified(string cacheKey, CacheEventArg cacheEventArgs)
    {
        switch (cacheEventArgs.EventType)
        {
            case EventType.ItemAdded:
                Console.WriteLine(
                    $"Item with Key '{cacheKey}' has been added to cache '{cacheEventArgs.CacheName}'"
                );
                break;


            case EventType.ItemUpdated:
                Console.WriteLine(
                    $"Item with Key '{cacheKey}' has been updated in the cache '{cacheEventArgs.CacheName}'"
                );


                // Item can be used if EventDataFilter is DataWithMetadata or Metadata
                // In this example, this property is set only for Item-Level events since we registered with EventDataFilter to DataWithMetadata
                // but for Cache-Level events the EventDataFilter is set to None, which returns only Keys
                if (cacheEventArgs.Item != null)
                {
                    Book updatedBook = cacheEventArgs.Item.GetValue<Book>();
                    Console.WriteLine(
                        $"Updated Item is a Book having name '{updatedBook.Name}', price '{updatedBook.Price}', and quantity '{updatedBook.AvailableUnits}'"
                    );
                }
                break;


            case EventType.ItemRemoved:
                Console.WriteLine(
                    $"Item with Key '{cacheKey}' has been removed from the cache '{cacheEventArgs.CacheName}'"
                );
                break;
        }
    }


    /// <summary>
    /// Register callback notification for cache level events
    /// Need to unregister events when no more needed using the returned CacheEventDescriptor
    /// API Reference - https://www.alachisoft.com/resources/docs/ncache/dotnet-api-ref/Alachisoft.NCache.Client.Services.IMessagingService.RegisterCacheNotification.html
    /// </summary>
    public CacheEventDescriptor RegisterCacheNotificationsForAllOperations()
    {
        // create CacheDataNotificationCallback object
        var dataNotificationCallback = new CacheDataNotificationCallback(OnCacheDataModified);


        // Register cache notification with "ItemAdded" EventType and
        // EventDataFilter "None" which means only keys will be returned
        CacheEventDescriptor eventDescriptor = cache.MessagingService.RegisterCacheNotification(
            dataNotificationCallback,
            EventType.ItemAdded | EventType.ItemUpdated | EventType.ItemRemoved,
            EventDataFilter.None
        );


        if (eventDescriptor.IsRegistered)
        {
            Console.WriteLine("Cache level notifications registered successfully");
        }


        return eventDescriptor;
    }


    /// <summary>
    /// Register callback notification for a single or an array of cache keys
    /// API Reference - https://www.alachisoft.com/resources/docs/ncache/dotnet-api-ref/Alachisoft.NCache.Client.Services.IMessagingService.RegisterCacheNotification.html
    /// </summary>
    /// <param name="cacheKey"></param>
    public void RegisterCacheNotificationsForSingleKey(string cacheKey)
    {
        // create CacheDataNotificationCallback object
        var dataNotificationCallback = new CacheDataNotificationCallback(OnCacheDataModified);


        // Register notifications for a specific item being updated in cache
        // EventDataFilter as DataWithMetadata returns keys along with their entire data
        cache.MessagingService.RegisterCacheNotification(
            key,
            dataNotificationCallback,
            EventType.ItemUpdated,
            EventDataFilter.DataWithMetadata
        );
    }


    public void UnRegisterCacheNotificationsForAllOperations()
    {
        // Unregister Notifications using the EventDescriptor
        cache.MessagingService.UnRegisterCacheNotification(_cacheLevelEventsDescriptor);
    }
}

Conclusion

Leveraging event-driven architecture in applications brings real-time responsiveness and scalability. Cache notifications are pivotal for maintaining data consistency and synchronization. They help in enhancing application performance and reducing latency.

In this article, we have seen the advantages Cache Event Notifications provide for developing responsive and scalable applications. We have also discussed briefly on the key considerations to be made while designing such a system.

Various cache providers, including Redis, Memcached, Hazelcast, Apache Ignite, and NCache, offer support for cache notifications. We have looked into how Cache Event Notifications work in NCache and its features.

NCache supports Data Specific, Cache Level, Item Level, Management Level, and Client Activity Events. Implementing these notifications with NCache and .NET involves subscribing to events and enabling notifications through NCache Manager. 

By implementing cache event notifications, we can enhance the performance, reliability, and responsiveness of event-driven systems.