<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Games24x7 Blogs on Medium]]></title>
        <description><![CDATA[Stories by Games24x7 Blogs on Medium]]></description>
        <link>https://medium.com/@Games24x7Tech?source=rss-8e0c59a39d31------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*82SWbAEPN7K3ZoNnNDR6Eg.jpeg</url>
            <title>Stories by Games24x7 Blogs on Medium</title>
            <link>https://medium.com/@Games24x7Tech?source=rss-8e0c59a39d31------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 01:20:43 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@Games24x7Tech/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[From Java 8 to Java 21: A Step-by-Step Migration Guide]]></title>
            <link>https://medium.com/@Games24x7Tech/from-java-8-to-java-21-a-step-by-step-migration-guide-24ec6b41f3ac?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/24ec6b41f3ac</guid>
            <category><![CDATA[java]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Wed, 27 Aug 2025 12:56:14 GMT</pubDate>
            <atom:updated>2025-08-27T12:56:14.866Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*md6ZZ4TQBb2nJlW49ggJ4A.png" /></figure><h3>Introduction</h3><p>At Games24x7, we deliver seamless, high-performance gaming experiences for millions of players, making system performance and scalability our top priorities. For many years, our backend was built on Java 8. However, as technology evolved, we recognized the necessity to upgrade. Java 21 introduced powerful new features and long-term support, marking the ideal moment for this transition. In this blog, we share our real-world migration journey from Java 8 to Java 21, detailing the challenges we overcame, the solutions that succeeded, and the improvements achieved.</p><p>If your applications are still running on Java 8, you’re not just missing out on cleaner syntax and enhanced performance, you’re also accumulating technical debt, exposing yourself to potential security risks, and facing increasing incompatibility with modern libraries and frameworks.</p><p>In this guide, we will walk you through how we migrated our large-scale production systems from Java 8 to Java 21, detailing what broke, what worked, and what we learned. Whether you’re planning a migration or are simply curious about the changes, this blog will provide you with a roadmap filled with real-world insights.</p><h3>🚀 Why Migrate to Java 21?</h3><p>With evolving technology and growing player demands, staying on Java 8 was no longer sustainable. Java 21 brought us the opportunity to modernize our stack, eliminate legacy constraints, and unlock powerful features like virtual threads, enhanced garbage collection, and long-term support. This migration wasn’t just an upgrade — it was a strategic step toward faster performance, tighter security, and future-ready scalability.</p><h3>⚠️ Challenges with Java 8:</h3><p>Remaining on Java 8 presented considerable risks to our operations:</p><ul><li><strong>End of Support<br></strong>Java 8 has reached the end of public updates under the OpenJDK project, and Oracle’s commercial support is ended. Continuing on this older version left our systems at risk of missing important security patches and performance improvements. Upgrading was essential to ensure long-term stability and maintainability.</li><li><strong>Security Vulnerabilities<br></strong>Java 8 lacks modern cryptographic algorithms and misses out on critical security patches provided in newer versions. This increases the attack surface and poses a serious risk to player data and financial transactions.</li><li><strong>Framework Incompatibilities<br></strong>Modern frameworks like Spring Boot 3.x, Jakarta EE, and Jetty 12 require Java 17 or above. This made it impossible to adopt essential tools for performance, scalability, and observability while staying on Java 8.</li><li><strong>Performance Limitations<br></strong>Garbage collectors in Java 8 (like CMS and early G1) are not optimized for today’s low-latency, high-throughput workloads. This impacted our ability to scale reliably during peak traffic.</li><li><strong>Technical Debt<br></strong>Maintaining legacy Java 8 code slows down developer velocity, increases boilerplate, and blocks adoption of cleaner, safer language features. This led to higher maintenance costs and slower feature delivery.</li></ul><h3>✅ Benefits of Migrating to Java 21:</h3><p>Java 21 solves these limitations while opening the door to modern, high-performance engineering:</p><ul><li><strong>Long-Term Support (LTS)<br></strong>With LTS support, Java 21 ensures consistent updates, stability, and security, critical for our production systems and compliance requirements.</li><li><strong>Enhanced InfoSec Compliance<br></strong>Transitioning to Java 21 allows us to eliminate vulnerabilities that were unresolvable within our outdated technology stack.</li><li><strong>Adoption of Modern Technology<br></strong>Earlier Java versions restricted our ability to utilize advanced, high-performance tech stacks such as Jetty 12, Jakarta Servlet APIs, and Virtual Threads.</li><li><strong>Advanced Garbage Collectors<br></strong>The introduction of new garbage collectors (ZGC and Shenandoah) are optimized for low-pause, high-throughput workloads, resulting in improved startup times, reduced memory usage, and optimized overall performance.</li><li><strong>Virtual Threads (Project Loom)<br></strong>By replacing traditional thread pools, virtual threads empower us to manage thousands of concurrent player connections with minimal overhead, significantly enhancing server scalability.</li><li><strong>Modern Java Features for Cleaner Code<br></strong>Java 21 offers developer-friendly features like var, records, text blocks, sealed classes, and pattern matching, which contribute to making code more readable, concise, and maintainable.</li><li><strong>Native Image Support via GraalVM<br></strong>Java 21 improves integration with native-image builds, facilitating ultra-fast startup times and reduced memory consumption for microservices.</li></ul><h3>🔍 Java 8 vs Java 21: Feature Comparison</h3><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fyYkweEKhk5LnLC8.png" /></figure><h3>🧭 Preparation and Planning</h3><p>Our migration journey began with a detailed audit of our codebase to assess Java 21 compatibility. Key preparation steps included:</p><ul><li><strong>Internal Library Analysis<br></strong>We reviewed all internal libraries for Java 21 compatibility. Special attention was given to usages of javax.*, since some annotations may compile without errors but silently fail at runtime.</li><li><strong>Build Tool Updates<br></strong>We upgraded Maven to version 3.10.x to ensure Java 21 support. Below is a sample maven-compiler-plugin configuration we used:</li></ul><pre>&lt;plugin&gt;<br> &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;<br> &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;<br> &lt;version&gt;3.11.0&lt;/version&gt;<br> &lt;configuration&gt;<br> &lt;source&gt;21&lt;/source&gt;<br> &lt;target&gt;21&lt;/target&gt;<br> &lt;/configuration&gt;<br>&lt;/plugin&gt;</pre><ul><li><strong>CI/CD Pipeline Configuration<br></strong>We modified our Jenkins pipelines to test builds against Adoptium’s Java 21 JDK, validating compatibility in isolated environments using Docker containers.</li><li><strong>Version Control<br></strong>All migration-related changes were isolated in a dedicated Git branch, allowing for safe experimentation, and easy rollback if necessary.</li><li><strong>Created New Infrastructure<br></strong>To minimize risk, we deployed Java 21 builds to a separate infrastructure. This allowed us to safely route live traffic back to the Java 8 environment in case of issues, ensuring zero downtime during transition.</li></ul><p>These steps ensured our codebase was ready for Java 21 without disrupting our high-concurrency gaming workloads.</p><h3>🎯 Migration Approach</h3><p>Given the scale and complexity of our codebase, we followed a phased migration strategy to ensure stability and minimize risk.</p><ol><li><strong>Library Compatibility Audit<br></strong>We analyzed all external libraries for Java 21 compatibility. For outdated or unsupported dependencies, we referred to official migration documentation and upgraded them selectively, prioritizing business-critical components.</li><li><strong>Internal library Upgrade<br></strong>Updated internal library to make it compatible with java 21. One major change involved migrating from javax.* to jakarta.*, especially to align with Spring Boot 3.x and Jakarta EE 10+.</li><li><strong>Build Compatibility<br></strong>Initially, we focused on ensuring that services built and ran correctly on Java 21 without adopting any new Java 21 language features. New feature we integrated separately.</li><li><strong>Automated &amp; Regression Testing<br></strong>We ran extensive unit, integration, and end-to-end test suites to verify functionality and catch regressions early.</li><li><strong>Load Testing &amp; Benchmarking<br></strong>Performance testing was conducted under production-simulated loads to compare Java 8 vs Java 21 metrics, including memory usage, GC behavior, latency and verified there is no memory leak issue.</li></ol><h3>🧩 Dependency Compatibility for Java 21 &amp; Spring Boot 3.x</h3><p>As part of the migration, we upgraded all unsupported third-party and internal dependencies to ensure seamless compatibility with Java 21 and Spring Boot 3.x:</p><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/0*ol3jVKLpTo0wrjAw.png" /></figure><h3>🛠️ Challenges Faced and Solutions Implemented</h3><p>Upgrading a large-scale production system from Java 8 to Java 21 came with lots of challenges. Here’s how we tackled them:</p><ul><li><strong>Jetty 12 Migration<br></strong>Jetty 12 introduced breaking changes in its WebSocket API. We restructured our WebSocket handling logic to align with the new server model, ensuring seamless bidirectional communication.</li><li><strong>Jakarta Namespace Transition<br></strong>The shift from javax.* to jakarta.* necessitated extensive refactoring of our codebase, updating Spring Boot configurations, and upgrading all dependent internal libraries to support Jakarta EE.</li><li><strong>Internal Library Compatibility<br></strong>Some internal and third-party libraries relied on deprecated or restricted internal Java APIs. We resolved this by either upgrading to compatible versions or refactoring the code to use supported public APIs.</li><li><strong>Akka Version Conflict<br></strong>The incompatibility between Akka 2.12 and 2.16 made it hard to deploy services independently. We resolved this by decoupling Akka-based communication between services to avoid taking downtime.</li><li><strong>MongoDB Server Compatibility<br></strong>The new MongoDB Java driver required a minimum server version of 4.0. To avoid mismatches, we upgraded our MongoDB clusters in both staging and production.</li><li><strong>Swagger API Deprecation<br></strong>springfox-swagger was incompatible with Java 21 and Spring Boot 3.x. We migrated to springdoc-openapi, which fully supports the new ecosystem while offering equivalent functionality.</li></ul><h3>📝 Code Transformations</h3><p>While migrating to Java 21, we had the opportunity to modernize parts of our codebase using new language features. These changes are <strong>optional</strong>, older Java 8-style code still works but adopting them can lead to cleaner, more expressive code.</p><p><em>Example 1:</em> var <em>usage (Java 10)</em></p><pre>// Before<br>Map&lt;String, String&gt; config = new HashMap&lt;&gt;();<br>// After<br>var config = new HashMap&lt;String, String&gt;();</pre><p>Example 2: Switch Expressions (Java 14)</p><pre>// Before<br>switch (status) {<br> case SUCCESS:<br> result = &quot;OK&quot;;<br> break;<br> case ERROR:<br> result = &quot;FAIL&quot;;<br> break;<br>}<br>// After<br>result = switch (status) {<br> case SUCCESS -&gt; &quot;OK&quot;;<br> case ERROR -&gt; &quot;FAIL&quot;;<br>};</pre><p><em>Example 3: Records for Immutable Data Classes (Java 16)</em></p><pre>// Before<br>public class User {<br> private final String name;<br> private final int age;<br> // constructor, getters, equals, hashCode, toString<br>}<br>// After<br>public record User(String name, int age) {}</pre><p>Example 4: Pattern Matching for instanceof <em>(Java 16)</em></p><pre>// Before<br>if (animal instanceof Cat) {<br> Cat cat = (Cat) animal;<br> cat.meow();<br>}<br>// After<br>if (animal instanceof Cat cat) {<br> cat.meow();<br>}</pre><h3>📈 Improvements Observed</h3><p>As part of our migration to Java 21, we observed significant enhancements across performance, security, and code maintainability:</p><ul><li><strong>Performance Gains<br></strong>We have observed a <strong>40–80%</strong> reduction in latencies across various scenarios. For instance, the FMG search response time has improved by approximately 50<strong>%</strong> (p99 reduced from 200 ms to 80–90 ms). Graphs are available for reference.</li><li><strong>Vulnerability Reduced<br></strong>We successfully reduced over <strong>2500 vulnerabilities</strong>, including <strong>180 critical</strong>and <strong>1200 high</strong> severity issues across our services. This was achieved by upgrading dependencies and adopting Java 21</li><li><strong>Cleaner, More Maintainable Code<br></strong>The new language features have encouraged the development of more maintainable and readable code.</li></ul><h3>Performance Metrics</h3><p>To validate the impact of our migration, we benchmarked key services <strong>before and after upgrading to Java 21</strong>. Below are some notable improvements</p><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CqRu62H5MCuN9wGg" /></figure><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BVKbjphkqBS8wWVs" /></figure><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cZ05gGz89Hb3L6yq" /></figure><h3>💡Tips for Others Planning Similar Migrations</h3><ul><li><strong>Start Small</strong>: Begin with utility modules or internal libraries before touching core services. This reduces blast radius and builds confidence.</li><li><strong>Invest in Testing</strong>: A robust test suite (unit, integration, load) is essential to catch regressions early and validate performance improvements.</li><li><strong>Upgrade Dependencies Early</strong>: Identify and upgrade incompatible libraries up front. This clears the path for Java 21 compatibility and avoids obstacles mid-way.</li><li><strong>Educate the Team</strong>: Conduct training sessions to familiarize the team with new Java 21 features. Also, communicate key migration changes clearly to avoid incompatible code in future development and reduce bugs.</li><li><strong>Document the Process</strong>: Keep detailed records of the migration steps and decisions made to help future migrations and onboarding of new team members.</li></ul><h3>Conclusion</h3><p>Migrating from Java 8 to Java 21 has been a significant milestone for us at Games24x7. This upgrade unlocked modern language features, stronger security, better performance, and access to new concurrency models like virtual threads — all critical for running high-scale gaming systems.</p><p>Looking ahead, we’re excited to further adopt Java 21 capabilities such as <strong>records</strong>, <strong>virtual threads</strong>, and <strong>advanced garbage collectors</strong> to boost developer productivity and system efficiency.</p><p>If your systems are still running on Java 8, now is the right time to migrate. The Java ecosystem has evolved, tooling support is mature, and the long-term benefits in performance, maintainability, and security significantly outweigh the migration effort.</p><h3>References</h3><p><a href="https://doc.akka.io/docs/akka/2.5/project/migration-guide-2.4.x-2.5.x.html">https://doc.akka.io/docs/akka/2.5/project/migration-guide-2.4.x-2.5.x.html</a></p><p><a href="https://doc.akka.io/docs/akka/2.6/project/migration-guide-2.5.x-2.6.x.html">https://doc.akka.io/docs/akka/2.6/project/migration-guide-2.5.x-2.6.x.html</a></p><p><a href="https://doc.akka.io/japi/akka/2.4/deprecated-list.html">https://doc.akka.io/japi/akka/2.4/deprecated-list.html</a></p><p><a href="https://doc.akka.io/japi/akka/2.5/deprecated-list.html">https://doc.akka.io/japi/akka/2.5/deprecated-list.html</a></p><p><a href="https://github.com/spring-projects/spring-boot/releases/tag/v3.2.1">https://github.com/spring-projects/spring-boot/releases/tag/v3.2.1</a></p><p><a href="https://jetty.org/docs/jetty/12/programming-guide/migration/11-to-12.html">https://jetty.org/docs/jetty/12/programming-guide/migration/11-to-12.html</a></p><p><a href="https://jetty.org/docs/jetty/12/programming-guide/migration/94-to-10.html">https://jetty.org/docs/jetty/12/programming-guide/migration/94-to-10.html</a></p><p><a href="https://jetty.org/docs/jetty/12/programming-guide/server/websocket.html">https://jetty.org/docs/jetty/12/programming-guide/server/websocket.html</a></p><p><a href="https://thorben-janssen.com/migrating-to-hibernate-6/">https://thorben-janssen.com/migrating-to-hibernate-6/</a></p><p><a href="https://www.oracle.com/news/announcement/ocw-oracle-releases-java-21-2023-09-19/?utm_source=chatgpt.com">https://www.oracle.com/news/announcement/ocw-oracle-releases-java-21-2023-09-19/?utm_source=chatgpt.com</a></p><p><a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html</a></p><p><a href="https://springdoc.org/migrating-from-springfox.html?utm_source=chatgpt.com">https://springdoc.org/migrating-from-springfox.html?utm_source=chatgpt.com</a></p><h3>About the Author</h3><p><a href="https://www.linkedin.com/in/satyamjaiswal1/"><strong>Satyam Jaiswal</strong></a> is a Software Engineer at Games24x7, with 3 years of experience in backend development. He is currently part of the RummyCircle backend team, where he works on building scalable, high-performance systems. Satyam is an alumnus of NIT Raipur, where he has completed his B.Tech in Information Technology. He enjoys solving real-world engineering challenges and contributes to delivering seamless gaming experiences at scale.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=24ec6b41f3ac" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Bitmap Magic: Storing 100 Million User Mappings Efficiently]]></title>
            <link>https://medium.com/@Games24x7Tech/bitmap-magic-storing-100-million-user-mappings-efficiently-165fe752a366?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/165fe752a366</guid>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Thu, 31 Jul 2025 04:05:20 GMT</pubDate>
            <atom:updated>2025-09-08T08:50:14.870Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jPOk_g8NXyLlIp-aYnOL1Q.png" /></figure><p>At <strong>Games24x7</strong>, operating around the clock is the baseline, not the goal. The real challenge — and the mark of success — is achieving massive scale and unwavering reliability without letting operational costs spiral out of control. While many solutions focus on adding more hardware, the secret to true, sustainable efficiency is often hidden deeper in the architecture. This post is a living example of that principle, showcasing how one of the most fundamental choices an engineer can make — the selection of the right data structure — can become the cornerstone of a cost-effective and highly responsive system.</p><p>In this deep dive, we will pull back the curtain on a critical architectural decision that has had a profound impact on our operations. We’ll start by outlining the specific challenges we faced with tracking millions of concurrent users and their activities. We will then compare the data structures we evaluated, explaining why a particular choice proved to be a game-changer. Finally, we’ll share the tangible results, demonstrating how this single decision led to a significant reduction in memory footprint, lower infrastructure costs, and faster performance, proving that the smartest architecture is the key to winning at scale.</p><h3>Understanding Custom Visible Tournaments (CVTs): Targeted Gaming Experiences</h3><p>In the highly competitive gaming industry, <strong>personalisation is key</strong>. Custom Visible Tournaments (CVTs) enable gaming companies to create tournaments and campaigns specifically tailored to target distinct player cohorts, offering gaming experiences that resonate with diverse player bases. For example, a CVT could target high-value players with exclusive tournaments or introduce beginner-friendly competitions to onboard new users.</p><h3>Diving Deeper into Custom Visibility and Dynamic Entry Fees</h3><p>Beyond just targeting broad player segments, <strong>Custom Visible Contests/Tournaments</strong> empower platforms to tailor the user experience at a granular level. Imagine showing specific games only to players from a particular state due to local regulations, or offering exclusive, high-stakes challenges solely to your most engaged users. This isn’t just about showing a game; it’s about <strong>reducing clutter</strong> and presenting <strong>only the most relevant opportunities</strong> to each player, dramatically enhancing their engagement.</p><p>Taking this personalisation further, <strong>Dynamic Entry</strong> <strong>Fees</strong> enable the adjustment of contest offerings based on a user’s financial profile. This means high-rollers might exclusively see high-entry, high-reward tournaments, while players who prefer lower stakes are presented with more accessible, medium-prize alternatives that match their comfort level. Moreover, leveraging a user’s <strong>current wallet balance</strong> ensures they only see contests they can genuinely afford to join, eliminating frustration and creating a smoother, more enjoyable player journey. This intelligent filtering and fee adjustment capability is crucial for maximising participation and fostering long-term player retention across diverse user segments.</p><p>However, as user base grow and campaigns become more ambitious, managing the sheer volume of data associated with these tournaments can become a logistical and technical challenge. With millions of users and thousands of tournaments in play, optimising how this data is stored and accessed becomes a mission-critical task.</p><p>This blog explores how we revamped our CVT architecture to solve storage issues, improve performance, and cut costs by leveraging bitmap data structures — a solution that reduced storage needs by 1000x while ensuring scalability.</p><h3>Current Architecture for our system:</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*njxTV975jXl4DYkD" /></figure><h3>Challenges We Faced in Storing User Data in CVTs</h3><p>The original CVT architecture was plagued by redundancy and inefficiency. To illustrate the scale of the problem:</p><p><strong>Repetitive Data Storage:</strong> For every tournament in a campaign, a separate list of user IDs was created and stored in Redis. In scenarios where a campaign included 1,000 tournaments targeting the same 10 million users, this led to the creation of 1,000 individual sets containing identical user data. The resulting overhead was immense, consuming hundreds of gigabytes of memory unnecessarily.</p><p><strong>Performance Bottlenecks:</strong> Redis operations, such as searching or updating user participation in these tournaments, were slow and resource-intensive. Checking whether a user belonged to a tournament involved scanning large sets, leading to increased latency as the dataset grew.</p><p><strong>Operational Challenges and Business:</strong> For large campaigns spanning weeks, these inefficiencies not only caused Out of Memory (OOM) errors but also significantly increased the cost of infrastructure due to the need for additional resources.</p><p><strong>Team Level Challenges:</strong> When the system ran out of memory, our Engineering Team had to wake up in the middle of the night to identify and manually delete big targeted campaigns. This process was sometimes error-prone.</p><p><strong>Operational Team Challenges:</strong> Due to these issues, the Operational team could not create campaigns longer than two days. They spent a lot of time on operational work, sometimes even during weekends, instead of focusing on strategic initiatives.</p><p>Clearly, a fundamental redesign was needed to address these storage and performance challenges.</p><h3>Guiding Principles for the New Architecture:</h3><p>While designing the new architecture, one of our guiding principles was <strong>meeting the growing demand of the business</strong>. When looking for a solution, we wanted the storage cost to be <strong>independent of the growing demand of the business and user base</strong>. We also wanted to ensure that our <strong>Service Level Agreements (SLAs) remained the same or improved</strong> compared to the older system.</p><h3>Solutions Evaluated:</h3><h4>Any disk-based databases like MongoDB:</h4><p>We explored using a disk-based database like MongoDB, keeping cost in mind. However, we faced <strong>latency issues with MongoDB at this humangous data scale</strong>. Both insert and get operations were taking O(n) time. As for every Campaign ID, we needed to store all user IDs in a new document. With growing data, we would also need to scale disk/MongoDB clusters continuously. Therefore, MongoDB did not meet our guiding principle for the new architecture, specifically the need for fast, O(1) operations.</p><h4>Went back to drawing board:</h4><p>Then we went back to the drawing board and asked ourselves a few key questions:</p><ol><li>What kind of access patterns do we truly need?</li><li>If we want to create a solution where storage cost doesn’t increase with growing business, we need a solution where we <strong>can’t store the user IDs themselves</strong>.</li></ol><p>At this point, we asked ourselves:</p><ul><li>Do we truly need to store individual User IDs?</li><li>Our core use case is simply to determine if a given CVT is <strong>available for a specific player</strong> (i.e., a boolean check).</li><li>This implied that if we could somehow store a Boolean value for each User ID (indicating their inclusion in a campaign), that would be sufficient.</li><li>We then discussed keeping a Boolean array with a size equal to our user base, where the array index would represent the Player ID, and the Boolean value at that location would tell if the player is part of the campaign or not.</li><li>In this model, <strong>updating and getting values would both be O(1) time operations</strong>.</li><li>This became a game-changing realisation for us! We then discovered that <strong>Redis Bitmaps</strong> perform exactly this job efficiently. As we were already using Redis and it’s inherently fast, it became our default choice.</li></ul><h3>Bitmap to the Rescue: How We Reduced Storage by 1000x</h3><p>Enter the <strong>bitmap</strong> — a powerful and efficient data structure in Redis. Unlike traditional storage methods, the size of a bitmap depends on the <strong>range of user IDs</strong> (i.e., the maximum user ID) rather than the number of users it stores. This makes it an ideal solution for use cases where the user ID space is large but sparsely populated.</p><h3>Key Benefits of Bitmaps</h3><h4><strong>Storage Efficiency with User ID Ranges:</strong></h4><p>Instead of creating multiple sets for each tournament, we now store a <strong>single bitmap per campaign</strong>. Each bit in the bitmap corresponds to a possible user ID, with ‘1’ indicating that the user belongs to the campaign and ‘0’ indicating otherwise.</p><p>The size of a bitmap in bytes is calculated as: Bitmap size (bytes)=⌈Max User ID/8⌉</p><p>Let’s analyze the storage required for different user ID ranges:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/596/0*b0WZnG3GFK6G0BsI" /></figure><p>This demonstrates how memory usage scales linearly with the range of user IDs, making bitmaps an incredibly efficient choice for storing data about large user populations.</p><h4>Direct Storage Comparison: Old vs. New Solution</h4><p>The impact of this architectural shift on storage efficiency is staggering across various user base scales. Consider a scenario where a campaign targets a significant portion of our user base:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fbLQ-0xhMoQhqpny0-fwfw.png" /></figure><p>This table vividly illustrates the massive reduction in memory footprint. The new architecture’s memory usage is primarily dictated by the <em>maximum possible user ID</em> your system supports, rather than the sheer number of active users within each individual campaign or tournament. This dramatic improvement ensures scalability and significant cost savings, especially when considering campaigns that encompass hundreds or thousands of tournaments all targeting the same user base.</p><blockquote><strong>Lightning-Fast Operations:</strong> Bitmaps enable <strong>O(1) time complexity</strong> for both read and write operations. Checking whether a user ID belongs to a campaign or updating their participation status is instantaneous, even for massive datasets.</blockquote><blockquote><strong>Scalability:</strong> The size of the bitmap does not depend on the actual number of users but rather on the <strong>maximum user ID</strong> in the dataset. This ensures that the memory footprint remains predictable and manageable, even as our user base grows to hundreds of millions or billions of users.</blockquote><h3>Cost Savings and Scalability: The Future of CVTs</h3><h3>Cost Analysis:</h3><p><strong>Under the old system:</strong></p><ul><li>A Redis cluster of 10 nodes (r5.xlarge, 32GB memory) was required, costing approximately <strong>$1,814/month</strong>.</li><li>Storage for 10 million users per tournament consumed 500MB, significantly limiting the number of tournaments that could be attached to campaigns.</li></ul><p><strong>With the new architecture:</strong></p><ul><li>A smaller Elasticache cluster (cache.m6g.large, 6.38GB memory) meets our needs at just <strong>$643/month</strong> — a <strong>70% cost reduction</strong>.</li><li>Storage for 30 campaigns (with each 10 million users) now requires only <strong>~960MB</strong>.</li></ul><h3>Future Scalability:</h3><ul><li>The bitmap solution is <strong>future-proof</strong>, capable of supporting up to 4.2 billion user IDs with minimal additional memory.</li><li>Marketing teams can now create <strong>year-long campaigns with hundreds of tournaments</strong>, a capability previously unfeasible due to storage limitations and operational overhead.</li></ul><h3>Operational Simplicity:</h3><p>No complex data migration was required for this architectural shift. The new architecture could be deployed during a scheduled downtime window with minimal disruption, demonstrating its elegance and efficiency.</p><h3><strong>About the author:</strong></h3><p><a href="https://www.linkedin.com/in/sumit-kumar-7a0412162">Sumit Kumar</a> is a Senior Developer at Games24x7, where he serves as the technical owner of the entire Rummy games ecosystem. He specializes in designing and building highly scalable backend systems, translating complex user journeys into elegant, high-performance architecture. Outside of his technical leadership, Sumit enjoys a good game of cricket and unwinding with true-crime docuseries.</p><p><a href="https://www.linkedin.com/in/surajs121/">Suraj Sharma</a> is a Senior Software Architect at Games24x7 with over 12 years of experience in designing and building cloud-native, internet-scale software.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=165fe752a366" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Games24x7: Where Trust Meets Gameplay]]></title>
            <link>https://medium.com/@Games24x7Tech/games24x7-where-trust-meets-gameplay-f477bb8715f3?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/f477bb8715f3</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[game-development]]></category>
            <category><![CDATA[games]]></category>
            <category><![CDATA[trust]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Wed, 23 Jul 2025 08:07:45 GMT</pubDate>
            <atom:updated>2025-07-23T08:07:45.284Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/509/0*gUMq5vCOYokZSvru" /></figure><p>At Games24x7, trust isnʼt just a principle — itʼs the foundation of everything we do. Whether you’re strategizing on RummyCircle, crafting a winning team on My11Circle, rolling the dice in Ludo, or sharpening your Poker skills, we’ve got one mission: making every moment thrilling, secure, and seamless.</p><p>Behind the scenes, advanced technology fuels the magic, ensuring every interaction on our platform is smooth, reliable, and unforgettable. Hereʼs how we’re building a platform youʼll love and trust.</p><h3>Trust and Privacy: The Pulse of Games24x7</h3><p>Trust isnʼt given; itʼs earned. Thatʼs why we go above and beyond to safeguard every aspect of your experience. From encrypting Personally Identifiable Information (PII) to implementing strict access controls, your data is always protected.</p><p>Handling millions of transactions daily, we’ve perfected the art of combining scalability with security. This means you never have to choose between a safe experience and top-tier performance. With Games24x7, you get the best of both worlds.</p><h3>Smooth Sailing from Registration to Gameplay</h3><h4>Registration, Simplified</h4><p>Signing up on Games24x7 is a breeze. Whether you choose email-password, mobile OTP, or email OTP, you’ll be in the game in no time. Add a personal flair with a unique username — select from options like Ace, Alpha, King, or Clown — or let the system pick one for you.</p><p>We take authenticity seriously. Mobile verification ensures every player is real, creating a bot-free, genuine gaming community. From the moment you join, it’s clear this is a platform built for trust, fun, and fair play.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*R5qCLZ381M6ajezr" /></figure><h3>Streamlined and Dynamic KYC Verification</h3><p>Completing your KYC? Simple and secure. Upload documents or verify via OTP — itʼs all quick and user-friendly. We cross-check details against official databases, guaranteeing accuracy and compliance.</p><p>KYC isnʼt just about ticking boxes; itʼs our way of creating a trusted space for genuine competition. Every verified user means fewer fake accounts and more transparent, fraud-free tournaments</p><h4>Why KYC Matters: Fair Play for All</h4><p>At Games24x7, KYC isnʼt just about compliance — it’s about creating a trusted environment where everyone can play with confidence. By ensuring every user is verified, we eliminate fake players, keeping real-money tournaments fair, transparent, and free from fraud.</p><h3>Ensuring Data Integrity and Compliance</h3><p>Trust goes hand in hand with vigilance. We conduct regular checks to prevent duplication, verify age (ensuring all players meet the 18+ requirement), and adhere to local regulations. Combined with our secure data storage practices, these measures uphold the integrity of our platform, guaranteeing a transparent and legally compliant gaming experience.</p><p>With KYC, you’re not just verifying your identity — you’re stepping into a community built on fairness, trust, and a commitment to authentic gameplay.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/883/0*3zgddc_qvuuTGGsK" /></figure><h3>Fast, Transparent, and Secure Transactions</h3><p>In the dynamic world of online gaming, speed and reliability are everything. Thatʼs why our financial systems are built to handle transactions with lightning-fast precision. Whether it’s deposits, withdrawals, or contest prizes, every transaction is processed in seconds — because who wants to wait when the fun is just a click away?</p><h4>Safety You Can Count On</h4><p>At Games24x7, your transactions are always in safe hands. With state-of-the-art security protocols, advanced encryption, and robust fraud detection systems, we ensure your money is secure every step of the way. So, you can focus on the game without a momentʼs worry.</p><h4>Transparency That Builds Trust</h4><p>We believe in keeping things crystal clear. Our transaction history page provides a complete view of every deposit, game table transaction, and withdrawal. No hidden fees, no surprises — just full transparency and a platform you can trust.</p><p>With Games24x7, you can rest easy knowing your transactions are not only fast but also secure and transparent, leaving you free to enjoy the thrill of the game.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/1*iknWDeATc94wQd-1MK4LCA.png" /></figure><h4>Cutting-Edge Monitoring for Flawless Operation</h4><p>Our systems are built for precision:</p><ul><li><strong>Real-Time Settlement Monitoring — </strong>No missed transactions</li><li><strong>Continuous Ledger Tracking: </strong>Every rupee accounted for.</li><li><strong>Daily Reconciliation Alerts: </strong>Issues resolved seamlessly, often before you notice.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/655/0*hGbCbvkBXtCH8ykS" /></figure><h3>Real-Time Notifications: Built on Trust, Designed for You</h3><p>At Games24x7, trust forms the backbone of everything we do, including how we communicate with you. Our push notifications are more than just timely updates — they reflect our dedication to transparency, dependability, and delivering personalised experiences.</p><p>Explore the details in this blog: <a href="https://aws.amazon.com/blogs/gametech/how-games24x7-delivers-millions-of-push-notification-using-aws/">How Games24x7 Delivers Millions of Push Notifications Using AWS</a>.</p><h4><strong>Why You Can Rely on Our Notifications:</strong></h4><p>Precision at Scale: When itʼs time to announce an upcoming tournament or share an exclusive offer, you can count on us. Our robust system delivers up to 14 million notifications within minutes, ensuring you’re always in the know, no matter what.</p><p>Tailored Transparency: From updates on your favorite games to reminders about your winnings, each notification is carefully curated to provide information that matters to you — nothing more, nothing less.</p><p>Real-Time Reliability: With our real-time alerts, youʼll never miss a critical moment. Whether itʼs game-changing news or a time- sensitive promotion, we ensure every notification reaches you promptly and accurately.</p><p>At Games24x7, our advanced notification system is more than just technology — it’s a promise. A promise to keep you engaged, informed, and always ahead, so you can play with confidence and peace of mind.</p><p>Because when trust leads the way, the fun never stops.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*amS4aTllHIF_rjMY" /></figure><h3>Is Games24x7 Really Legit? A Behind-the-Scenes Story</h3><p>Imagine this: You’re sitting with friends, scrolling through the internet, when someone says, “Can you <em>really </em>trust games like RummyCircle or My11Circle? I mean, ₹3 Crore prize pools? Fair gameplay? Sounds too good to be true.”</p><p>You pause, curious. It’s a valid question. And this is where the story begins — a story of trust, technology, and the people behind the magic of Games24x7.</p><h4>Doubt 1: “Is the ₹3 Crore prize on My11Circle even real?”</h4><p>One of our developers smiles and says, “<em>Absolutely. Let us break it down for you.</em>”</p><p>At My11Circle, everything is governed by a transparent system. Our contests, including the ₹3 Crore first prize, operate under publicly available rules that leave no room for doubt. Your teamʼs score is based on the real-world performance of players, and our backend runs a highly secure, scalable architecture that ensures every point is logged accurately in real time.</p><p>Hereʼs the deal: Once a match locks, your team is set in stone — no last-minute changes, no hidden adjustments. The leaderboard updates dynamically in real time as points are scored, and prizes are credited to the winners almost instantly after the match concludes.</p><p>Discover the full story here: <a href="https://medium.com/%40Games24x7Tech/distributed-scoring-and-ranking-through-thor-a2d64eb0edfe">Distributed Scoring and Ranking Through Thor</a>.</p><p>To make it even more transparent, participants can download all teams of a contest after the match begins. This feature allows anyone to cross-verify the fairness of results once the match is concluded.</p><blockquote>“You can literally watch your rank climb as players perform on the field,” the developer adds. “Itʼs like watching magic, only itʼs powered by math, code, and trust.”</blockquote><h4><strong><em>Clearing the Air About Our Fantasy Game Prizes</em></strong></h4><p>We understand that big prize pools like ₹3 Crore can raise questions. Is it for real? Are the winners legitimate? Is the prize money actually paid out?</p><p>To ensure transparency, we’ve launched our Winners Page, where you can view real winners, their profiles, and prize amounts. Youʼll also find details on the total prize pool, white goods awarded, and nearby winners.</p><p>Our goal is to give you confidence in the fairness and authenticity of our contests. By showcasing genuine winners, weʼre building trust and excitement — because when you win, you win for real!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/1*lWAj-1TKY_8iLTbWMph-rA.png" /></figure><h4><strong>Doubt 2: “What about fairness? How do I know itʼs not rigged?”</strong></h4><p>Another team member chimes in, “<em>Ever heard of RNG-certified systems?</em>”</p><p>For games like RummyCircle and Ludo, randomness isnʼt left to chance — itʼs algorithmic and certified. Our card shuffle systems on RummyCircle are designed to be tamper-proof, ensuring every deal is completely random. And when it comes to Ludo, our Random Number Generator (RNG) ensures that every dice roll is genuinely unpredictable.</p><p>“Itʼs like a digital vault,” they explain. “Even though we built the system, we’ve designed it to be completely tamper-proof. Itʼs powered by industry-standard security practices, including recommendations from iTechLabs, a trusted certification authority. Thatʼs how we ensure your data and experience remain secure and reliable.”</p><blockquote>And what about suspicious activity? Our fraud detection tools are no joke. Using machine learning and real-time monitoring, we scan every move for patterns of bot usage or collusion. If something doesnʼt look right, itʼs flagged instantly, and our system locks out any fraudulent activity before it impacts the game.</blockquote><h4>Doubt 3: “Okay, but what about fake players in real-money tournaments?”</h4><p>This time, a data engineer steps in with a confident smile. “<em>Thatʼs why KYC is a non-negotiable step for us.</em>”</p><p>Before you can even join a game, you pass through a rigorous verification process. Mobile number verification, official ID checks, and age validation ensure that every player is real and meets our strict eligibility criteria (yes, we confirm all users are 18+).</p><p>“Every player you compete against is genuine,” the engineer assures. “No bots, no fraud — just authentic competition at its finest.”</p><p>But we donʼt stop there. Games24x7 has implemented a multi-layered fraud detection framework, as detailed in our <a href="https://medium.com/%40Games24x7Tech/fortress-of-fair-play-stopping-frauds-at-games24x7-7e8b928266b5">Fortress of</a> <a href="https://medium.com/%40Games24x7Tech/fortress-of-fair-play-stopping-frauds-at-games24x7-7e8b928266b5">Fair Play</a> initiative. This initiative uses advanced machine learning algorithms, real-time monitoring, and secure OTP verification to ensure the integrity of every match and transaction.</p><p>Through these systems, we can quickly detect and prevent suspicious activities like bot usage, collusion, or duplicate accounts. Our team continuously monitors for anomalies, ensuring that every player competes on a fair and level playing field.</p><blockquote>The engineer concludes with a reassuring tone: “Whether itʼs through KYC or cutting-edge fraud prevention, we’re committed to maintaining the fairness and trust that our players deserve. So when you play, you can focus on the thrill of the game — weʼve got the rest covered.</blockquote><h4>Doubt 4: “Can my transactions really be safe?”</h4><p>The head of security leans forward with a grin. “<em>You bet they are.</em>”</p><p>Every deposit, withdrawal, and prize credit is encrypted using the latest security protocols. Our transaction monitoring system tracks millions of data points daily, flagging anything unusual in real time. And for added transparency, you can view every deposit, game transaction, and withdrawal on your transaction history page — no hidden fees, no surprises.</p><blockquote>“Our systems are designed to process withdrawals in seconds,” they add. “Because your winnings are yours, and you shouldnʼt have to wait for them.”</blockquote><h4>Doubt 5: “But what happens if the system breaks?”</h4><p>A backend engineer chuckles and responds, “<em>Break? Thatʼs not even in our vocabulary.</em>”</p><p>At Games24x7, resilience isnʼt just a feature; itʼs a cornerstone of our platform. Our systems are engineered to handle massive traffic seamlessly, even during the most intense moments.</p><ul><li><strong>Push Notifications at Scale: </strong>Need updates? No problem! Our platform can send more than 14 million push notifications within minutes, ensuring every player stays informed, no matter the stakes.</li><li><strong>Contest Joins: </strong>Our backend seamlessly manages high-stakes contests, even during peak activity. We’ve processed up to 14 million contest joins with a peak throughput of 4.94K requests per second (RPS) — all without a hitch. Learn more here: <a href="https://medium.com/%40Games24x7Tech/serving-millions-of-contest-join-requests-my11circle-154541029c09">Serving Millions of Contest Join Requests — My11Circle</a>.</li><li><strong>Auto-login: </strong>When itʼs game time, players flood in to check contests or winnings. Our systems process app openings at a blistering speed of 2.75K RPS, ensuring no delays in getting you into the action.</li><li><strong>Efficient Payouts: </strong>The best part? Once the match is over, winnings are credited almost instantly post-settlement. From leaderboard rankings to prize transfers, every process is optimized for speed and accuracy.</li></ul><p>How do we make it happen? Our secret sauce includes distributed databases for data integrity, event-driven architectures for lightning-fast response times, and robust failover mechanisms to eliminate downtime. Real-time observability tools continuously monitor everything — from transaction logs to pending settlements — so we catch and resolve issues before you even notice them.</p><blockquote>The engineer wraps up with a confident grin: “Whether itʼs push notifications, contest joins, or crediting your winnings, we’ve built our systems to handle the pressure. So you focus on the game — weʼll make sure everything else works like clockwork.”</blockquote><h3>The Big Picture</h3><p>As the conversation wraps up, one of the team members leans back and says, <em>“Look, we know trust isnʼt given — itʼs earned. Thatʼs why weʼve built every system, every feature, and every line of code with one goal in mind: to give you a platform where you can play with confidence.”</em></p><p>So the next time someone questions whether Games24x7 is legit, youʼll know the answer. Behind every game, every leaderboard update, and every prize is a team of developers, engineers, and security experts working around the clock to create a platform thatʼs fair, transparent, and built for you.</p><p>The only thing left to do? Dive in and see the magic for yourself.</p><h3>The Games24x7 Promise</h3><p>At Games24x7, we blend cutting-edge technology with an unwavering commitment to trust and transparency. From seamless registration and quick payouts to secure gameplay, every part of your experience is designed with you in mind.</p><p>So, whether you’re aiming for the top of the leaderboard or just enjoying a casual game, you can count on us to deliver a platform thatʼs fair, fun, and built for you.</p><blockquote>Ready to play with confidence? Letʼs roll!</blockquote><h3><strong>About the Authors</strong></h3><p><a href="https://www.linkedin.com/in/ankit-sharma-5b1b35158/"><strong>Ankit D Sharma</strong></a> is an SDE-2 at Games24x7, part of the Platform team. Passionate about coding challenges, he frequently explores emerging technologies, participates in hackathons, and has a particular affinity for AWS solutions.</p><p><a href="https://www.linkedin.com/in/murali4u45/"><strong>Murali Mohanty</strong></a> is an Engineering Manager at Games24x7 with a B.Tech degree from BPUT. He specializes in problem-solving and excels at building and maintaining distributed systems.</p><p><a href="https://in.linkedin.com/in/nitiprabhu"><strong>Nithish Prabhu</strong></a>, an SDE-3, is a tech enthusiast building scalable systems at Games24x7. With a passion for cutting-edge technologies, he enjoys solving complex software challenges.</p><p><a href="https://www.linkedin.com/in/rohit-kakulapati-20b42a104/"><strong>Rohit Kakulapati</strong></a>, is an SDE-3 at Games24x7 and an alumnus of the Birla Institute of Technology, Pilani. With expertise in building scalable backend applications, he also enjoys watching football and reading in his free time.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f477bb8715f3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unleash the Power of AI for Mindmapping Spec Documents]]></title>
            <link>https://medium.com/@Games24x7Tech/unleash-the-power-of-ai-for-mindmapping-spec-documents-ea01464bba68?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/ea01464bba68</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[testing]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 14:36:41 GMT</pubDate>
            <atom:updated>2025-07-16T14:36:41.000Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*npOiIPe8ezMfbdPV" /></figure><h3>Abstract</h3><p>In modern software development, ensuring robust and reliable testing is crucial for delivering high-quality products. Traditional manual creation of test cases (TCs) from user stories often becomes time-consuming and error-prone. This paper proposes an innovative solution that leverages <strong>Claude Sonnet 3.0 model via</strong> <strong>AWS Bedrock</strong>, a state-of-the-art generative AI model, for the automatic creation of detailed test cases from specification documents. The solution is integrated into an existing self-serve portal, where Quality Assurance (QA) teams can upload user story documents and receive test case mind maps, facilitating better test coverage and faster delivery cycles. We demonstrate how Claude Sonnet model enhances the process through natural language processing (NLP) to generate structured test cases, mapped into mind map format for easy integration into QA workflows. This approach offers a significant leap toward automating and streamlining test case generation, resulting in enhanced testing efficiency and quality.</p><h3><strong>1. Introduction</strong></h3><p>In any software development lifecycle, <strong>test case generation</strong> is essential to ensure that the software behaves as expected under various conditions. Typically, test cases are manually written based on <strong>user stories</strong>, which describe the features or functionalities of the software in natural language. However, this manual process can be time-consuming, inconsistent, and prone to human error. With the advent of artificial intelligence (AI), there is a growing opportunity to automate repetitive tasks such as test case generation.</p><p>This paper discusses a solution we developed at our organization that integrates <strong>Claude Sonnet</strong>, a powerful generative AI model, to automate the creation of test cases from user stories provided in specification documents. The generated test cases are output in a <strong>mind map format</strong>, offering QA teams a visual representation that is easy to navigate. The solution leverages the AWS ecosystem, specifically <strong>Claude Sonnet 3.0 via </strong>AWS <strong>Bedrock</strong>, to achieve high accuracy and efficiency in test case generation.</p><h3>2. Problem Statement and Motivation</h3><p>Quality Assurance (QA) teams play a vital role in maintaining the reliability and performance of software products. However, the creation of test cases — essential for validating each user story — often becomes a major bottleneck. Writing test cases manually is time-consuming, labor-intensive, and susceptible to human error. It also lacks consistency across teams and projects, especially as the complexity of systems grows.</p><p>This challenge becomes even more pressing during the Indian Premier League (IPL) season, a critical period for <em>My11Circle</em>, the flagship fantasy sports platform of Games24x7. With millions of users participating in real-time, feature stability and performance are non-negotiable. In IPL 2024 alone, <em>My11Circle</em> saw a <strong>50% spike</strong> in team creations within the first two weeks and a similar increase in mega prize winners. Looking ahead to IPL 2025, user activity is expected to reach new heights.</p><p>To keep up with this demand, the product and engineering teams must release numerous high-impact and innovative features — often under tight deadlines. QA engineers are then required to review massive specification documents, frequently spanning 100–150 pages, and turn them into comprehensive test cases within a limited time frame. This creates immense pressure, often resulting in missed edge cases and defects slipping into production.</p><p>Recognizing this challenge, we asked ourselves a fundamental question:<br><strong>Why not use AI to assist QA in this era of intelligent automation?</strong></p><p>To address these pain points, we’ve begun leveraging <strong>Claude Sonnet via</strong> <strong>AWS Bedrock</strong> to automate test case generation. By converting dense product specifications into structured, mind map-based test cases, we aim to:</p><ul><li>Reduce manual effort for QA teams</li><li>Ensure greater consistency and coverage across test scenarios</li><li>Accelerate feature delivery without compromising quality</li></ul><p>This initiative not only enhances testing efficiency but also empowers our QA teams to focus on higher-value work, ensuring <em>My11Circle</em> remains reliable and resilient during its most critical business window — the IPL season.</p><h3>3. System Architecture and Workflow</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RkbwYK5G3gvQQzGH" /></figure><h3>3.1. Overview of the System</h3><p>The solution we propose integrates into an existing <strong>self-serve portal</strong> used by QA teams in our organization. The user journey is as follows:</p><ol><li><strong>Upload Spec Document</strong>: QA teams upload a <strong>user story specification document</strong> (in .docx format) that contains detailed user stories and associated requirements.</li><li><strong>Automated Test Case Generation</strong>: Claude Sonnet is used to process the user story, extract key details, and generate structured test cases in a predefined JSON format.</li><li><strong>Mind Map Creation</strong>: The generated test cases are then converted into a <strong>mind map</strong> (saved as a .mup file) which visualizes the relationships between user stories, test cases, and various attributes such as <strong>steps</strong>, <strong>pre-conditions</strong>, and <strong>expected results</strong>.</li><li><strong>Download Mind Map</strong>: Once the process is completed, the mind map is automatically made available for download, providing a comprehensive, easy-to-read structure for the QA team to review and use in testing.</li></ol><h3>3.2. System Components</h3><ol><li><strong>Frontend (UI/UX)</strong>:</li></ol><ul><li>A user-friendly card interface on the web portal allows QA teams to upload their .docx specification files.</li><li>After the file is successfully processed, the option to download the generated mind map becomes available.</li></ul><ol><li><strong>Backend (API Layer)</strong>:</li></ol><ul><li><strong>File Handling</strong>: The backend handles file uploads, validating that the document is in the .docx format and saving it for processing.</li><li><strong>AWS Bedrock Integration</strong>: The backend service integrates with Claude Sonnet through a series of APIs to send user story content and receive generated test cases.</li><li><strong>Mind Map Generation</strong>: The backend processes the test case data and maps it into a structured mind map format, which is then saved as a .mup file.</li></ul><ol><li><strong>Claude Sonnet 3.0 </strong>:</li></ol><ul><li>Claude Sonnet via AWS Bedrock is utilized to generate detailed test cases from the specification document. We designed a prompt that guides the model to produce the test cases in a strict JSON format that includes the <strong>description</strong>, <strong>pre-conditions</strong>, <strong>steps</strong>, and <strong>expected results</strong> for each test case.</li></ul><h3>3.3. Integration Flow</h3><ol><li><strong>Step 1: User Story Extraction</strong>: The .docx file is parsed, and relevant user stories are extracted. The content is then provided to <strong>Claude Sonnet </strong>in a conversational format, where each user story is processed individually.</li><li><strong>Step 2: Test Case Generation</strong>: Claude Sonnet model processes the provided content and generates structured test cases in JSON format. The response follows the predefined structure, which includes a detailed set of steps, pre-conditions, and expected results for each test case.</li><li><strong>Step 3: Mapping to Mind Map Format</strong>: The test case data is converted into a <strong>mind map</strong> structure that uses nodes and sub-nodes to represent the user stories and their respective test cases. Each test case becomes a node in the mind map, with related attributes like description, steps, and expected results.</li><li><strong>Step 4: File Generation and Download</strong>: Once the mind map is created, it is saved as a .mup file and made available for download via the frontend.</li></ol><h3>4. Claude Sonnet’s Role in Test Case Generation</h3><p>The central component in the success of this system is <strong>Claude Sonnet model</strong>. <strong>Claude Sonnet 3.0 </strong>is an advanced generative AI model that excels in natural language processing (NLP) and can understand and respond to complex prompts. The model plays the following critical roles:</p><ol><li><strong>Text Understanding</strong>: Model processes each user story and generates logical, structured responses based on the input prompts. This enables the AI to create highly accurate test cases that cover both positive and negative scenarios.</li><li><strong>Structured Output</strong>: The model was trained to strictly follow the provided JSON format, ensuring that the generated test cases are structured in a way that is both easy to process and useful for QA teams.</li><li><strong>Contextual Reasoning</strong>: By breaking down each user story into manageable chunks and providing them one at a time to the model, the system ensures that model can reason through each story more thoroughly, thus improving the quality of the test cases.</li><li><strong>Scalability and Flexibility</strong>: Model’s ability to scale to handle multiple user stories and generate relevant test cases in parallel makes it ideal for integration in large-scale systems.</li></ol><h3>5. Results and Evaluation</h3><p>Example :</p><p><strong>User story</strong> :</p><p>As a user, I want an option to stay logged in, so that I don’t have to enter my credentials every time. As a user, I want to be able to reset my password if I forget it, so that I can regain access to my account.</p><p><strong>Prompt to Claude Sonnet model</strong> :</p><p>You are an expert tester. Your task is to create detailed positive and negative test cases in JSON format. We will provide you with various fields and scenarios, and you must respond with test cases in the following structure:</p><pre>{<br>  &quot;Test Cases&quot;: [<br>    {<br>      &quot;Description&quot;: &quot;&quot;,<br>      &quot;Pre Condition&quot;: &quot;&quot;,<br>      &quot;Steps&quot;: &quot;&quot;,<br>      &quot;Expected Result&quot;: &quot;&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;&quot;,<br>      &quot;Pre Condition&quot;: &quot;&quot;,<br>      &quot;Steps&quot;: &quot;&quot;,<br>      &quot;Expected Result&quot;: &quot;&quot;<br>    }<br>  ]<br>}</pre><p>As a user, I want an option to stay logged in, so that I don’t have to enter my credentials every time. As a user, I want to be able to reset my password if I forget it, so that I can regain access to my account.</p><p><strong>The Model’s Response?</strong></p><pre>{<br>  &quot;Test Cases&quot;: [<br>    {<br>      &quot;Description&quot;: &quot;Verify the &#39;Stay Logged In&#39; option works correctly when selected&quot;,<br>      &quot;Pre Condition&quot;: &quot;User has a valid account and is on the login page&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter valid username and password&quot;,<br>        &quot;2. Select the &#39;Stay Logged In&#39; checkbox&quot;,<br>        &quot;3. Click the &#39;Login&#39; button&quot;,<br>        &quot;4. Close the browser&quot;,<br>        &quot;5. Reopen the browser and navigate to the application&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User is logged in automatically without needing to enter credentials&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the &#39;Stay Logged In&#39; option does not keep the user logged in when not selected&quot;,<br>      &quot;Pre Condition&quot;: &quot;User has a valid account and is on the login page&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter valid username and password&quot;,<br>        &quot;2. Do not select the &#39;Stay Logged In&#39; checkbox&quot;,<br>        &quot;3. Click the &#39;Login&#39; button&quot;,<br>        &quot;4. Close the browser&quot;,<br>        &quot;5. Reopen the browser and navigate to the application&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User is prompted to enter credentials again&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the &#39;Stay Logged In&#39; option does not work with invalid credentials&quot;,<br>      &quot;Pre Condition&quot;: &quot;User is on the login page&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter invalid username or password&quot;,<br>        &quot;2. Select the &#39;Stay Logged In&#39; checkbox&quot;,<br>        &quot;3. Click the &#39;Login&#39; button&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User sees an error message indicating invalid credentials and is not logged in&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the password reset option works with a valid email&quot;,<br>      &quot;Pre Condition&quot;: &quot;User is on the &#39;Forgot Password&#39; page&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter a registered email address&quot;,<br>        &quot;2. Click the &#39;Reset Password&#39; button&quot;,<br>        &quot;3. Check the email for a password reset link&quot;,<br>        &quot;4. Click the link and set a new password&quot;,<br>        &quot;5. Log in with the new password&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User receives an email, resets the password successfully, and logs in with the new password&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the password reset fails for an unregistered email&quot;,<br>      &quot;Pre Condition&quot;: &quot;User is on the &#39;Forgot Password&#39; page&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter an unregistered email address&quot;,<br>        &quot;2. Click the &#39;Reset Password&#39; button&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User sees an error message indicating the email is not registered&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the password reset link expires after the specified time&quot;,<br>      &quot;Pre Condition&quot;: &quot;User has requested a password reset&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Request a password reset&quot;,<br>        &quot;2. Wait for the reset link to expire (e.g., 24 hours)&quot;,<br>        &quot;3. Attempt to use the expired reset link&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User sees an error message indicating the link has expired and is prompted to request a new reset link&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the password reset process fails if the new password does not meet complexity requirements&quot;,<br>      &quot;Pre Condition&quot;: &quot;User has requested a password reset and clicked the reset link&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Enter a new password that does not meet complexity requirements (e.g., too short or lacks special characters)&quot;,<br>        &quot;2. Submit the new password&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User sees an error message indicating the password does not meet the complexity requirements&quot;<br>    },<br>    {<br>      &quot;Description&quot;: &quot;Verify the user is logged out of all devices after resetting the password&quot;,<br>      &quot;Pre Condition&quot;: &quot;User is logged in on multiple devices and initiates a password reset&quot;,<br>      &quot;Steps&quot;: [<br>        &quot;1. Reset the password using the &#39;Forgot Password&#39; option&quot;,<br>        &quot;2. Attempt to use the application on other devices without logging in again&quot;<br>      ],<br>      &quot;Expected Result&quot;: &quot;User is logged out of all devices and prompted to log in with the new password&quot;<br>    }<br>  ]<br>}</pre><p><strong>Generated mindmap</strong> :</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5471lkx59qIqsl8s" /></figure><h3><strong>5.1. User Adoption and Feedback</strong></h3><p>Since deploying this solution, the <strong>QA teams</strong> have reported significant improvements in both the speed and quality of test case generation. The key benefits highlighted include:</p><ul><li><strong>Reduced Manual Effort</strong>: QA engineers no longer need to manually write test cases, which reduces the workload and the likelihood of human error.</li><li><strong>Increased Consistency</strong>: Test cases are structured in a standardized format, improving the consistency and completeness of the testing process.</li><li><strong>Faster Test Coverage</strong>: The time taken to generate test cases from user stories has been reduced by <strong>over 50%</strong>, allowing QA teams to begin testing earlier in the development cycle.</li></ul><h3>5.2. Performance Metrics</h3><p>We conducted performance evaluations on the solution and observed the following results:</p><ul><li><strong>Processing Time</strong>: The time required to process a user story specification document and generate the corresponding mind map is under <strong>5 minutes</strong> on average.</li><li><strong>Accuracy</strong>: The test cases generated by Claude Sonnet 3.0 model adhered to the JSON format with a <strong>high degree of accuracy</strong>, providing a structured set of test cases that matched the input prompts.</li></ul><h3>5.3. Scalability</h3><p>The solution has been tested to handle specifications containing <strong>hundreds of user stories</strong>, with Claude Sonnet processing each in a scalable and efficient manner. This ensures that the solution can be scaled to larger teams and organizations with minimal performance degradation.</p><h3>6. Future Work</h3><p>While the current solution is highly effective, there are several opportunities for improvement and expansion:</p><ol><li><strong>Integration with Other Tools</strong>: Future versions could integrate with test management tools such as <strong>Jira</strong> or <strong>TestRail</strong> to automatically create and link test cases within existing test plans.</li><li><strong>Advanced Test Case Optimization</strong>: We could explore using Claude Sonnet model to generate more optimized test cases, focusing on edge cases and specific functional aspects of the user stories.</li><li><strong>Multilingual Support</strong>: Expanding the solution to support multiple languages could make it useful for global teams working with specifications in different languages.</li></ol><h3>7. Conclusion</h3><p>This blog presents an innovative solution for automating the generation of test cases using <strong>Claude Sonnet model</strong>, integrated with a mind map visualization format. The system significantly improves the speed, accuracy, and consistency of the test case generation process, reducing the manual effort involved and enhancing the efficiency of QA teams. By leveraging AI and cloud technologies, this solution streamlines the testing workflow and helps organizations ensure high-quality software releases.</p><h3><strong>About author :</strong></h3><p><a href="https://www.linkedin.com/in/nikita-jethani-12350a8b/">Nikita</a> brings 9 years of experience in automation testing, with a strong passion for emerging technologies and AI innovations. She actively explores cutting-edge solutions to enhance testing efficiency, streamline processes, and drive continuous improvement across the QA landscape.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea01464bba68" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Fixed a Huge ANR in Our My11Circle App: A Fun Deep Dive Into Solving Android Vitals…]]></title>
            <link>https://medium.com/@Games24x7Tech/how-we-fixed-a-huge-anr-in-our-my11circle-app-a-fun-deep-dive-into-solving-android-vitals-82e73e7efcdc?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/82e73e7efcdc</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[react]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Fri, 13 Jun 2025 12:33:09 GMT</pubDate>
            <atom:updated>2025-06-13T12:33:09.610Z</atom:updated>
            <content:encoded><![CDATA[<h3>How We Fixed a Huge ANR in Our My11Circle App: A Fun Deep Dive Into Solving Android Vitals Challenge 📱💥</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nvIOjuQFJgMCzpqcp6Iifw.png" /><figcaption><em>ANRs got you down? Here’s how we crushed them in My11Circle and improved our Android Vitals.</em></figcaption></figure><p><strong>TL;DR</strong>: Our team at Games24x7 faced a <strong>monstrous ANR</strong> issue that pushed our app’s Google Play Console metrics into the “bad behaviour” zone. After weeks of investigation and some “technical wizardry,” we not only crushed the ANR but also tackled a react-native bug specific to Android 12. Here’s how we did it.</p><h3>The Problem: A mysterious ANR without a clear cause 🤔</h3><p>It all started with a spike in <strong>ANRs</strong> in our <strong>My11Circle</strong> Android app. The situation got so bad that it pushed our metrics into the <strong>“bad behaviour”</strong> zone on Google Play Console 😱. The first issue? The <strong>root cause</strong> message was as cryptic as a riddle wrapped in an enigma. The error log read:<br>android.os.MessageQueue.nativePollOnce</p><pre>libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce + 44) (BuildId: cebd92605c5c36dc0013f1ca7b00edbd)<br>       at android.os.MessageQueue.nativePollOnce(Native method)<br>       at android.os.MessageQueue.next(MessageQueue.java:335)<br>       at android.os.Looper.loopOnce(Looper.java:186)<br>       at android.os.Looper.loop(Looper.java:313)<br>       at android.app.ActivityThread.main(ActivityThread.java:8663)<br>       at java.lang.reflect.Method.invoke(Native method)<br>       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)<br>       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)</pre><p>I mean, seriously — what does that even mean? 🧐</p><h3>The Breakthrough: Crashes leading to ANR 💡</h3><p>Then, after countless hours of stress testing (and probably some coffee-fueled breakdowns ☕💀), we got lucky. While trying to reproduce the ANR, we triggered a <strong>crash</strong> instead. But wait… instead of crashing as expected, the app entered an <strong>ANR</strong> state.</p><p>BINGO! 🎯 This was a crucial breakthrough. The issue wasn’t just a random crash; it was related to the crash handling mechanism. We were getting closer to the <strong>cause</strong>… and closer to <strong>saving the day </strong>(saving the week TBH 😅🙈).</p><h3>Uncovering the Root Cause: The faulty exception handler 🔧</h3><p>As we dug deeper, we discovered the villain behind the ANRs: our <strong>custom uncaught exception handler</strong>. Now, this handler was supposed to be our trusty sidekick 🦸‍♂️ — stepping in whenever things went wrong, catching exceptions, and showing the user an error screen with a helpful message and a “relaunch” button.</p><p><strong>In theory</strong>, it was brilliant. <strong>In practice</strong>, not so much. 🤷‍♂️</p><p>This was our implementation of exception handler:</p><pre>Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {<br><br>          @Override<br>          public void uncaughtException(Thread thread, Throwable throwable) {<br><br>            String stackTraceString = Log.getStackTraceString(throwable);<br>            callbackHolder.invoke(stackTraceString);<br>            activity = getCurrentActivity();<br>            Intent i = new Intent();<br>            i.setClass(activity, errorIntentTargetClass);<br>            i.putExtra(&quot;stack_trace_string&quot;,stackTraceString);<br>            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);<br>            activity.startActivity(i);<br>            activity.finish();<br>          }<br>      });</pre><p>The problem here is that when the app encountered a fatal exception, the handler wasn’t checking the activity state properly, and would try to launch a new activity even if the app was in the middle of a critical state (like destroying the current activity). This caused an <strong>ANR</strong> because the app couldn’t decide what to do next.</p><h3>The Fix: Refactoring the exception handler 🔨</h3><p>With the root cause identified, it was time to put on our <strong>problem-solving cap</strong> and <strong>refactor</strong> our handler. We were determined to stop the app from freezing and to handle uncaught exceptions <strong>like pros</strong>. 💼</p><p>Here’s what we did:</p><h4>1. Refactored the exception handler 🔄</h4><p>The handler got a little “<strong>refactor therapy</strong>” 💆‍♂️. We updated the logic to make sure we <strong>check the current activity state</strong> before launching a new one. And if the handler couldn’t manage the exception, we <strong>passed it to the original handler</strong>.</p><p>With that, the ANRs were <strong>reduced</strong> dramatically, and the app no longer froze when things went wrong. <strong>Victory!</strong> 🏆</p><p>This is our refactored implementation:</p><pre>originalHandler = Thread.getDefaultUncaughtExceptionHandler();<br>Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {<br><br>          @Override<br>          public void uncaughtException(Thread thread, Throwable throwable) {<br><br>            String stackTraceString = Log.getStackTraceString(throwable);<br>            callbackHolder.invoke(stackTraceString);<br>            activity = getCurrentActivity();<br>            if (activity == null || activity.isFinishing) {<br>              originalHandler?.uncaughtException(thread, throwable);<br>              return;<br>            }<br>            Intent i = new Intent();<br>            i.setClass(activity, errorIntentTargetClass);<br>            i.putExtra(&quot;stack_trace_string&quot;,stackTraceString);<br>            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);<br>            activity.startActivity(i);<br>            activity.finish();<br>          }<br>      });</pre><h4>2. Testing &amp; Validation ✅</h4><p>We didn’t just stop there. We made sure to <strong>stress test</strong> the app to simulate crashes and see if the fix worked. We also added logs and breadcrumbs to track issue on Firebase Crashlytics. We wanted to make sure our app was no longer <em>hanging</em> around. 🤸‍♂️</p><h3>The Aftermath: From ANRs to crashes 💥😱</h3><p>As the ANRs dwindled, a new problem arose: <strong>crash reports</strong> started flooding in. It turns out, many of the errors that were causing ANRs were now being <strong>logged as crashes</strong>. But hey, at least they were <strong>being logged</strong> now! 💬</p><p>This meant we had taken a step forward, but also uncovered <strong>another layer of issues</strong>. We weren’t done yet.</p><h3>The React Native Bug: The ConcurrentLinkedQueue conundrum 🧵</h3><p>As the new crashes rolled in, we discovered that the <strong>real culprit</strong> was a <strong>bug in the React Native Android library</strong> that was only causing issues on <strong>Android 12 devices</strong>. Specifically, the issue was tied to <strong>ConcurrentLinkedQueue</strong>, a data structure that was being used in the react-native and react-native-reanimated.</p><h4>The Issue: Dropped items on Android 12 🚨</h4><p>On Android 12, the <strong>ConcurrentLinkedQueue</strong> was behaving unpredictably. It would <strong>drop items</strong> under certain conditions, which would eventually lead to crashes. The behaviour was very hard to reproduce as it was not happening on all the devices and it was a very edge case scenario, making it even harder to track down. 😓</p><h4>The Solution: Replacing ConcurrentLinkedQueue with LinkedBlockingQueue 🛠️</h4><p>To fix this, we decided to “queue” up a plan. Our solution? Replace the rogue ConcurrentLinkedQueue with the much more reliable LinkedBlockingQueue. We took the issue into our own hands and built a custom version of react-android with this fix. Talk about “queue-trolling” the problem! 😂</p><h4>Here’s the plan:</h4><ol><li><strong>Cloning and forking the react native android library</strong>:<br>We forked the <strong>React Native Android</strong> repo from GitHub and checked out the commit that matched our version.</li><li><strong>Implementing the patch</strong>:<br>Here’s how we made it work:</li></ol><ul><li><strong>Android version check</strong>: We added a check to see if the app was running on Android 12 (API level 31). If it was, we used <strong>LinkedBlockingQueue</strong> instead of <strong>ConcurrentLinkedQueue</strong>.</li></ul><pre>private final Queue&lt;UIThreadOperation&gt; mQueue;<br>String versionAndroid = Build.VERSION.RELEASE;<br>if (versionAndroid.equals(&quot;12&quot;)) {<br>    mQueue = new LinkedBlockingQueue&lt;&gt;();<br>} else {<br>    mQueue = new ConcurrentLinkedQueue&lt;&gt;();<br>}</pre><ul><li><strong>Patch implementation</strong>: We patched the React Native Android library by updating the source code wherever <strong>ConcurrentLinkedQueue</strong> was used, making it conditional based on the Android version.</li></ul><p><strong>3. Building and hosting the custom library</strong>:<br>We had to set our own little “code bakery” to serve the freshest fix! 🍞. Since React Native uses a separate react-android library (and not the one from node_modules), we built and hosted it on our <strong>S3 server</strong>. Refer this for building react native from source: <a href="https://github.com/facebook/react-native/wiki/Building-from-source/29169d93c6ffc43cb7454bebc94761a859c1c450">Building from source</a></p><p><strong>4. Fixing dependency issues</strong>:<br>The fun didn’t stop there! Even after mentioning our version in dependency it was picking the old version of react-android. Then after some debugging we found that <strong>React Native’s gradle.properties</strong> file was locking the library to an old version, so we had to <strong>override</strong> it to point to our patched version. Here’s how we made it “snap” into place:</p><pre>.<br>.<br>diff --git a/node_modules/react-native/ReactAndroid/gradle.properties b/node_modules/react-native/ReactAndroid/gradle.properties<br>index 2ab4e65..7792733 100644<br>--- a/node_modules/react-native/ReactAndroid/gradle.properties<br>+++ b/node_modules/react-native/ReactAndroid/gradle.properties<br>@@ -1,4 +1,4 @@<br>-VERSION_NAME=0.71.13<br>+VERSION_NAME=0.80.11-SNAPSHOT-1<br> GROUP=com.facebook.react<br> .<br> .</pre><p>5. <strong>Patching react-native-reanimated:</strong></p><p>Our version of react-native-reanimated was fetching pre-built AAR files, so simply applying the patch wasn’t working. Nothing was coming easy to us — no mercy! 💥 So, we had to dive deeper and make changes to its build.gradle file to enable building from source. Once we got that working, we finally applied the final patch with the ConcurrentQueue fixes and build fixes. It felt like finally getting past the toughest level of a game. 🎮🏆. This is how our reanimated patch looks like:</p><pre>.<br>.<br>.<br>-def aar = detectAAR(REACT_NATIVE_MINOR_VERSION, JS_RUNTIME)<br>-boolean BUILD_FROM_SOURCE = shouldBuildFromSource(aar, JS_RUNTIME)<br>+def aar = null<br>+boolean BUILD_FROM_SOURCE = true<br> .<br> .<br> .<br>+import java.util.concurrent.LinkedBlockingQueue;<br>+import android.util.Log;<br>+import android.os.Build;<br> <br> public class NodesManager implements EventDispatcherListener {<br> <br>@@ -110,7 +113,7 @@ public class NodesManager implements EventDispatcherListener {<br> <br>   private RCTEventEmitter mCustomEventHandler;<br>   private List&lt;OnAnimationFrame&gt; mFrameCallbacks = new ArrayList&lt;&gt;();<br>-  private ConcurrentLinkedQueue&lt;CopiedEvent&gt; mEventQueue = new ConcurrentLinkedQueue&lt;&gt;();<br>+  private Queue&lt;CopiedEvent&gt; mEventQueue;<br>   private boolean mWantRunUpdates;<br> <br>   public double currentFrameTimeMs;<br>@@ -187,6 +190,14 @@ public class NodesManager implements EventDispatcherListener {<br>     mUIManager.getEventDispatcher().addListener(this);<br> <br>     mAnimationManager = new AnimationsManager(mContext, mUIImplementation, mUIManager);<br>+<br>+    String versionAndroid = Build.VERSION.RELEASE;<br>+    Log.i(&quot;VERSION21&quot;, &quot;NodesManager RNANIMATED&quot; + versionAndroid);<br>+    if (versionAndroid.equals(&quot;12&quot;)) {<br>+      mEventQueue = new LinkedBlockingQueue&lt;&gt;();<br>+    } else {<br>+      mEventQueue = new ConcurrentLinkedQueue&lt;&gt;();<br>+    }<br>   }<br>.<br>.<br>.</pre><h3>Testing the fix 🔍🔨</h3><p>Testing was a real “hunt the bug” situation! Since this was an elusive issue, we couldn’t just rely on automation tests alone. We needed a backup plan:</p><ul><li><strong>Automation: </strong>We ran all the automation suites on our build, performed regression testing, and verified that everything is working. Since this was not an easily reproducible scenario, we could not rely only on these results, as these test suits were working earlier as well. That’s why we had to figure out another approach; let’s see what it was.</li><li><strong>Decompiled APK: </strong>We wanted to make sure that the correct version of react-android is being used in the final build and has all our changes for the fix. This is how we did it:</li></ul><ol><li>Decompiled our APK using apktool, which extracted DEX files from our APK: apktool d -r -s universal.apk</li><li>Converted these DEX files to JAR using dex2jar tool: d2j-dex2jar universal/classes.dex</li><li>Explored these JAR files using JD-GUI tool, searching for all the classes in which we made changes and verified that those changes are there.</li></ol><p>After testing with both automation and APK decompiling, we were confident the fix was solid. It was like finding the final puzzle piece and slotting it into place. 🧩 With that, we rolled out our next release, and the issue was finally laid to rest. 🛌</p><h3>The Result: A smoother experience for our users 🎉</h3><p>After deploying the patch, we saw a <strong>huge drop in ANRs</strong>. The app was running smoothly, and users on Android 12 experienced fewer crashes. Our android vitals really improved and it felt like we’d finally crossed the finish line. 🏁 <strong>Win!</strong></p><h3>Conclusion: The journey is never really over 🚀</h3><p>What started as a vague ANR message turned into a full-blown investigation that required a series of <strong>fixes, patches, and some caffeine</strong>. While we successfully reduced ANRs and solved a major third-party library issue, we’re not stopping here. 🚀</p><p>In the next phase, we’ll focus on <strong>optimising app start times</strong> and <strong>squashing performance bottlenecks</strong>. Stay tuned for more posts on our journey to make <strong>My11Circle</strong> even more <strong>smooth</strong> and <strong>seamless</strong>. 🏃‍♂️💨</p><h3><strong>About the Author:</strong></h3><p>I’m an Android developer with 5+ years of experience wrangling Kotlin, dodging NullPointerExceptions, and surviving Gradle builds with my sanity (mostly) intact. When I’m not building apps, I’m sipping chai and explaining to friends for the hundredth time that I don’t actually fix phones.</p><p>📬 Connect with me on <a href="https://www.linkedin.com/in/rajabhargava">LinkedIn</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=82e73e7efcdc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unity Engine in Dynamic Feature Module — Here’s How We Did It!]]></title>
            <link>https://medium.com/@Games24x7Tech/unity-engine-in-dynamic-feature-module-heres-how-we-did-it-03f4ac5ba355?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/03f4ac5ba355</guid>
            <category><![CDATA[unity-game-development]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[unity]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Wed, 14 May 2025 11:19:22 GMT</pubDate>
            <atom:updated>2025-05-14T11:19:22.538Z</atom:updated>
            <content:encoded><![CDATA[<h3>Unity Engine in Dynamic Feature Module — Here’s How We Did It!</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ba6j1WM4LVx35Qkht_gzgg.png" /><figcaption><em>Struggling with Unity Engine in Dynamic Features Modules? We Cracked the Code!</em></figcaption></figure><p>One of the most significant challenges in mobile app development is optimising application size without compromising functionality and UX. In the case of <strong>My11Circle</strong>, we successfully reduced the app size from <strong>63.8MB to 28MB</strong> by employing <strong>Dynamic Feature Modules</strong> (DFM) for our pre-bundled additional game, <strong>RummyCircle</strong>. However, this journey was far from straightforward, particularly due to the complexities associated with <strong>native shared object (.so) files</strong> and the <strong>Unity Engine</strong>, which does not officially support dynamic features.</p><p>This blog post outlines the challenges we faced, the solutions we implemented, and how we achieved this substantial size reduction.</p><h3>THE IMPACT: 56% Decrease in App Size!</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QK-hRcBOlNFOTCvb.png" /></figure><p>Google Play console download size trend over time</p><p><strong>Before Optimization</strong></p><blockquote><strong><em>63.8MB</em></strong><em> (Single Base Module)</em></blockquote><p><strong>After Optimization</strong></p><blockquote><strong><em>28MB</em></strong><em> (Base) + </em><strong><em>~35MB</em></strong><em> (Dynamic Feature)</em></blockquote><p>Extracting Unity components for <em>RummyCircle</em> into a <strong>Dynamic Feature Module</strong> allowed us to significantly shrink the base APK size while maintaining all essential features for the primary game, <em>My11Circle</em>.</p><h3>Business Impact 🚀</h3><p>From a business perspective, every MB saved in app size directly boosts key acquisition metrics. In this case, reducing the download size by approximately 37MB led to a significant improvement in acquisition, enhancing user engagement and conversion rates.</p><h3>Dynamic Feature + Unity and Native Libraries</h3><h3>🤔 Why Isn’t This Simple?</h3><p>At first glance, you might wonder: <strong>“What’s the big deal? Shouldn’t it be easy to create a Dynamic Feature and include the Unity engine in it?”</strong></p><p>🚨 <strong>Well, NO!</strong> 🚨</p><h3>Problem #1: Unity Does Not Officially Support Dynamic Features</h3><p>A significant hurdle was the <strong>lack of official support</strong> for Dynamic Feature Modules from the Unity framework. Here is a link to a Unity forum discussion highlighting that <strong>there is no direct implementation for dynamic feature modules</strong>:<br>🔗 <a href="https://discussions.unity.com/t/dynamic-feature-modules-is-there-a-unity-implementaion-i-am-missing/765536/5">Unity Forum Discussion</a></p><p>This resulted in minimal official support, scarce online resources, and numerous dead-end StackOverflow inquiries from previous attempts by other developers. Consequently, we had to manually determine how to dynamically load <strong>.so files</strong> and assets required by <strong>Unity</strong> at runtime once the dynamic feature was installed.</p><h3>Problem #2: Handling Native .SO Files in Dynamic Features</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zgzGgOSWv5ocOV7k.png" /></figure><p>In typical use-case, <strong>UnityPlayer</strong> automatically loads the required .so<strong> files</strong> during initialization, so developers don’t have to worry about it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ozu-JNuEouv6-4aW.png" /></figure><p>💥 <strong>But in our case, this didn’t work!</strong> (<em>First major setback</em>)</p><p>The culprit? <strong>Split APKs installation path.</strong></p><p>When an Android app uses <strong>Dynamic Feature Modules</strong> containing native libraries, the <strong>Android framework splits the APKs</strong> during installation. This means:</p><p>Native .so files are placed inside a <strong>separate ABI-specific split APK</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Zi-L7oXrI51-HLXq.png" /></figure><p>Post Dynamic-Feature installation these files end up in the /splitcompat folder inside filesDir.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ITFyLh48zAkGyvJU.png" /></figure><p><strong>UnityPlayer failed to locate and load them </strong>⚠️</p><p>To address this, we had to <strong>manually extract and load these libraries</strong> at runtime after the dynamic feature module was installed.</p><p><strong>SplitInstallHelper was of No Help </strong>❌</p><p>Google suggests using <strong>SplitInstallHelper</strong> to automatically locate and load native libraries in dynamic features. However, in certain Android versions, it completely <strong>failed to locate</strong> the required .so files.</p><p>✅ <strong>Solution:</strong><br>We had to <strong>brute force locate these files</strong> at runtime and manually load them using <strong>SoLoader</strong> to ensure stability across all devices.</p><p><strong>Inconsistent Installation Paths: BundleTool vs. Play Store Installation </strong>❌</p><p>Another major challenge was the <strong>difference in installation behaviour</strong> between:</p><ul><li><strong>Local builds using</strong> bundletool</li><li><strong>Play Store console installation</strong> (Internal test track)</li></ul><p>The <strong>installation path for the split APK (dynamic feature) differed significantly in both cases</strong>, leading to build failures when testing via the Play Store console.</p><p>✅ <strong>Solution:</strong><br>Since the installed .so file path varied in both scenarios, we had to <strong>manually handle .so loading in both scenarios</strong>. We had to upload multiple builds on <strong>Play Store’s internal test track</strong> to ensure that fixes worked across different OS versions and OEMs.</p><h3>Problem #3: Asset Loading Issue</h3><p>Another significant issue was that <strong>Unity does not recognise the split APK path</strong> when loading assets from a dynamic feature. By default, Unity attempts to load assets from the base module, resulting in missing asset errors.</p><p>✅ <strong>Solution:</strong> Overriding getPackageCodePath() in UnityActivity<br>After extensive research and debugging, we resolved this by overriding the getPackageCodePath() method in UnityActivity. This allowed us to dynamically provide the correct path to Unity, ensuring it accurately loaded assets from the dynamic feature module.</p><pre>@Override<br>public String getPackageCodePath() {<br>    if (isDynamicFeatureInstalled()) {<br>        return getDynamicFeaturePath();<br>    }<br>    return super.getPackageCodePath();<br>}</pre><h3>Problem #4: Unity Engine Load Error</h3><p>Another unexpected challenge was <strong>missing directories upon installation</strong>, which caused Unity to fail at runtime. Specifically, Unity would not load correctly unless certain directories were present and detected by the Unity engine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/0*F6gSUnaqQLjFfjoU.png" /></figure><p>While the Unity team has stated that they do not support dynamic features, we persevered and ultimately found a solution! 😏</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/836/0*7epTuId6rIzAFrPH.png" /></figure><p>✅ <strong>Solution:</strong> Adding an Empty JSON File<br>Through logcat, we discovered that the Unity engine was unable to load the assets prepacked in Unity AAR. Upon inspecting the app installation folder on the device, we found that the required asset directory was missing.</p><p>After further investigation, we placed an empty.json file inside the assets/bin/Data/Managed directory of our base module. By adding this <strong>empty file</strong>, we prevented the directory from being deleted post-installation, ensuring the availability of Unity assets after the dynamic feature installation. This allowed the Unity engine to locate these assets and load them correctly.</p><h3>🎯 The Final Outcome</h3><p>By implementing <strong>Dynamic Feature Module</strong>, manually managing <strong>native libraries</strong>, and <strong>overriding Unity’s asset loading behavior</strong>, we successfully <strong>reduced the My11Circle app size from 63.8MB to 28MB </strong>😎</p><p>This resulted in:</p><blockquote>✔️ <strong>A 56% reduction</strong> in app size (from <strong>63.8MB → 28MB</strong>)<br>✔️ <strong>Improved acquisition &amp; conversion rates</strong><br>✔️ <strong>Faster downloads &amp; updates</strong><br>✔️ <strong>A seamless experience for both My11Circle &amp; RummyCircle</strong></blockquote><p>💡 <strong>Key Takeaway:</strong> Unity and Dynamic Feature Modules <strong>aren’t designed to work together</strong> — but with the right workarounds, <strong>massive app size reductions are possible!</strong></p><p>🚀 Have you tried optimizing Unity apps for Android? <strong>We’d love to hear your experiences!</strong></p><h3>About the Author:</h3><p><a href="http://www.linkedin.com/in/abhinav-pawar-917691117">Abhinav Pawar</a> has over 10 years of experience building mobile apps across various domains.<br>He thrives on solving complex problems with innovative, out-of-the-box solutions.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=03f4ac5ba355" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beating the Heat: How We Cooled Down Cassandra’s Hot Partitions to scale]]></title>
            <link>https://medium.com/@Games24x7Tech/beating-the-heat-how-we-cooled-down-cassandras-hot-partitions-to-scale-b3a25b1c5c1c?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/b3a25b1c5c1c</guid>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[cassandra]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[fantasy-sports]]></category>
            <category><![CDATA[scalability]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Fri, 02 May 2025 07:31:33 GMT</pubDate>
            <atom:updated>2025-05-02T07:31:33.615Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1LFAxgv--3UpnMB2" /></figure><h3>Introduction :</h3><p>In the fast-paced world of fantasy sports, My11Circle stands out by offering sports fans the thrill of building dream teams and competing for real rewards. But behind the scenes, it’s the robust backend technology that ensures a seamless experience, especially during high-stakes events like the IPL and World Cup.<br>With millions of users creating teams and joining contests in a single match, keeping leaderboards accurate and fast is no small task. To handle this massive scale, we use Apache Cassandra for storing leaderboard data. However, as contests grew bigger, we faced performance issues that challenged our scaling strategies.</p><h3>The Fantasy Sports Technical Challenge :</h3><p>Fantasy sports platforms like My11Circle face unique technical challenges:</p><ul><li><strong>Massive Data Volume</strong>: Contests can include millions of participants.</li><li><strong>Prompt Availability</strong>: Users expect to see final leaderboards immediately after matches conclude.</li><li><strong>Resource Efficiency</strong>: Optimizing database utilization to control infrastructure costs.</li><li><strong>Scaling Headroom</strong>: Accommodating growing contest sizes without architecture overhauls.</li></ul><p>These requirements demand a robust database solution that can handle the persistence and retrieval of large-scale leaderboard data.</p><h3>Our Leaderboard Architecture :</h3><p>At My11Circle, we use a two-tier approach for our leaderboard system:</p><ol><li><strong>Redis</strong>: Serves real-time leaderboard updates during active matches.</li><li><strong>Apache Cassandra</strong>: Persists final leaderboard data once matches are completed.</li></ol><p>This blog focuses on the Cassandra persistence layer, which faced scaling challenges as our user base grew.</p><h3>Understanding Cassandra’s Data Architecture :</h3><p>To appreciate the optimization we implemented, it’s important to understand how Apache Cassandra organizes and processes data.</p><h3>Cassandra’s Core Concepts :</h3><p><strong>Primary Key</strong>: Unlike traditional relational databases where a primary key simply identifies a row, in Cassandra, the primary key serves dual purposes:</p><ol><li>Uniquely identifies a row.</li><li>Determines data distribution across the cluster.</li></ol><p><strong>The Partition Key</strong>: This is the first component of the primary key that determines which node in the cluster will store the data. It’s crucial for performance and scalability.</p><p><strong>Clustering Key</strong>: Additional columns in the primary key that determine the sorting order within a partition.</p><h3>What is a Partition in Cassandra ?</h3><p>A partition is a fundamental unit of data storage in Cassandra, containing rows that share the same partition key. Understanding partitions is essential because:</p><ul><li>Partitions determine how data is distributed across nodes</li><li>They directly impact write performance</li><li>Properly sized partitions ensure balanced workload distribution</li></ul><h3>How Data is Partitioned :</h3><ol><li><strong>Partition Key Selection</strong>: The partition key (single column or composite) determines data placement.</li><li><strong>Hashing Process</strong>: Cassandra applies a hash function to the partition key to generate a token.</li><li><strong>Token Assignment</strong>: This token corresponds to a value on the ring. Each node in the cluster is responsible for a range of tokens.</li><li><strong>Data Distribution</strong>: It takes the token value and walks clockwise on the ring and whichever node it meets first, that is the node the data is stored in. This token-based distribution ensures data is evenly spread across the cluster.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g4T28yG0jDwJ0NIU" /></figure><h3>The Challenge : Hot Partitions at Scale</h3><h3>My11Circle’s Leaderboard System :</h3><p>At My11Circle, we run multiple contests within each match. These contests vary in size and prize pools:</p><ul><li><strong>Regular contests</strong>: Typically lakhs of participants.</li><li><strong>Mega contests</strong>: Can reach 20 million participants during Major sport events.</li></ul><p>Each contest has its own leaderboard, which is updated in Redis during live matches. Once a match concludes, we persist the final leaderboard in Cassandra through a Spark-based service.</p><h3>Our Original Table Design :</h3><p>In a match, each contest is uniquely identified by a contest_id. When users join a contest, they create teams (each with a team_id) and based on performance, each team receives a rank in the final standings.</p><ul><li>contest_id served as the partition key, determining how data was distributed across the Cassandra cluster</li><li>rank and team_id were clustering keys, organizing data within each partition</li></ul><p>This design seemed logical at first grouping all teams from the same contest together. However, it created a critical scaling limitation: all data for a single contest was stored in one partition, regardless of size. For mega contests with millions of participants, this resulted in extremely large partitions that overwhelmed individual Cassandra nodes.</p><h3>The Problem Emerges :</h3><p>As our platform grew, we began experiencing performance issues:</p><h4>Observed Metrics and Bottlenecks</h4><ul><li><strong>Uneven Resource Utilization</strong>: During high-traffic matches, some nodes were overworked while others remained idle.</li><li><strong>Node CPU Imbalance</strong>: We observed significantly higher CPU utilization on specific nodes in our Cassandra cluster, reaching 80% CPU while others remained at 30–40% during the same workload period.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wHmwblrIb_13bsR4" /></figure><p>CPU</p><ul><li><strong>Warning Logs</strong>: Examined server logs on impacted Cassandra node.<br>`WARN [CompactionExecutor] Writing large partition fantasy_keyspace/leaderboard:127937062 (438.982MiB)</li><li><strong>Partition Size Explosion</strong>: Mega contests created partitions exceeding 500–800 MB, far beyond Cassandra’s recommended 100 MB limit.</li></ul><figure><img alt="CF stats of leaderboard table" src="https://cdn-images-1.medium.com/max/1024/0*ONRDgEb8Ro7TJBy7" /><figcaption>CF stats of leaderboard table</figcaption></figure><ul><li><strong>Data persistence time : </strong>Data persistence time increased as load increased.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GkJpyGcJ30HefdDR" /><figcaption>Write ops</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oJM8GRqYnYyjLHC-" /><figcaption>Write latency</figcaption></figure><h3>Diagnosis : Hot Partitions</h3><p>The root cause became clear: our design was creating “hot partitions” , partitions that received disproportionately high write traffic compared to others. The collected metrics served as evidence of the issue.</p><h3>Why Partition Size Matters in Cassandra :</h3><p>Oversized partitions create several critical performance issues in Cassandra:</p><h4>1. Write Performance Degradation</h4><p>Large partitions require more resources to process updates, causing increased latency during leaderboard persistence.</p><h4>2. Memory Pressure</h4><p>Oversized partitions consume disproportionate amounts of memory, triggering more frequent garbage collection and affecting overall system performance.</p><h4>3. Compaction Overhead</h4><p>Cassandra’s compaction process struggles with large partitions, consuming excessive resources and potentially falling behind during peak periods.</p><h4>4. Node Imbalance</h4><p>Hot partitions create uneven workload distribution, making some nodes work significantly harder than others in the cluster.</p><h4>5. Scalability Ceiling</h4><p>As contest sizes grew with each tournament, we faced an architectural ceiling that couldn’t be efficiently solved through vertical scaling.</p><p>Reducing partition size was essential to create a stable, predictable, and cost-efficient database layer for our growing platform.</p><h3>The Solution : Bucketization for Better Distribution</h3><p>After analyzing our options, we implemented a distribution technique called “bucketization” to evenly spread data across partitions.</p><h3>Finding the Optimal Bucket Key :</h3><p>We needed to identify a bucketization value that would:</p><ul><li>Be readily available during query execution</li><li>Ensure uniform data distribution</li><li>Require minimal application modifications</li></ul><p>Since users must create teams to participate in contests, and team rankings are already available pre-query, we leveraged these rankings as our bucket key for optimal data distribution.</p><h3>Bucketization Implementation :</h3><p>Rather than relying solely on contest_id as the partition key, we enhanced our approach by incorporating a calculated bucket value:</p><p><strong>Bucket Formula:</strong> bucket = (rank / K) + 1</p><p>Through extensive load testing, we determined K = 10,000 provides ideal performance:</p><ul><li>Teams ranked 1–9,999 are assigned to bucket 1</li><li>Teams ranked 10,000–19,999 are assigned to bucket 2</li><li>This pattern continues for all rankings</li></ul><h3>New Table Schemas :</h3><p>Note the composite partition key (contest_id, bucket), which spreads data across multiple partitions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1O6vHTWO_K1ZY6QZ" /></figure><h3>Key Performance Improvements :</h3><ol><li><strong>Even Resource Distribution</strong>: CPU and memory usage became balanced across nodes.</li><li><strong>Enhanced Scalability</strong>: The system can now handle larger contests without expensive hardware upgrades. This has enabled us to scale horizontally, helping us save approximately 50% in costs during peak tournament seasons.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MWOA8dZ-SdzRZ8-L" /><figcaption>CPU Utilisation — cassandra nodes</figcaption></figure><p><strong>3. Eliminated Hot Partitions</strong>: No more partition size warnings or performance bottlenecks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*a_OBS-h4Wr9xwd4X" /><figcaption>CF stats — leaderboard table after Bucket Algorithm</figcaption></figure><p><strong>4. Write Efficiency</strong>: The 99% reduction in write latency ensures faster updates.</p><p>5. <strong>Faster Leaderboard Availability</strong>: The 33% improvement in data persistence time ensures the final leaderboard is visible to users sooner, enhancing their experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*auQQMpGG6LQMlrre" /><figcaption>Write ops</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SYODvnQSpTC3S3sN" /><figcaption>Write latency</figcaption></figure><h3>The Impact: Faster, Leaner and More Scalable :</h3><p>The results were dramatic :</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hsvBdDOhpLRtNCwRFztoRA.png" /></figure><h3>Implementation Strategy : Phased Rollout</h3><p>To minimize the risk, we implemented a phased rollout strategy:</p><ol><li><strong>New Table Creation</strong>:<br>- We created the new leaderboard table in the existing Cassandra cluster.</li><li><strong>ZooKeeper-Based Configuration</strong>:<br>- We used ZooKeeper to manage which matches/series would use the new table structure<br>- This allowed for gradual transition on a match-by-match basis<br>- Common configuration was deployed so multiple services could read from the same path</li><li><strong>Dual-Write/Read Logic</strong>:<br>- During settlement, our Spark service (Thor) would check if the matchId/seriesId was in the ZooKeeper config<br>- If included, data would be persisted to the new leaderboard table.<br>- For reads, our Leaderboard Service would check the same ZooKeeper config to determine which table to query for a particular matchId.</li><li><strong>Progressive Rollout</strong>:<br>- Started with small-scale matches first<br>- Gradually expanded to larger matches as confidence grew<br>- Monitored performance at each stage</li></ol><p>This approach allowed us to validate improvements in a controlled manner before full deployment.</p><h3>Lessons Learned :</h3><p>This optimization journey taught us several valuable lessons:</p><ol><li><strong>Data modeling is crucial </strong>: In distributed databases, how you structure your data is as important as the infrastructure itself.</li><li><strong>Anticipate growth </strong>: Design for future scale, not just current needs.</li><li><strong>Monitor partition sizes </strong>: Regular monitoring helps catch issues before they become critical.</li><li><strong>Simple solutions can have big impacts </strong>: Sometimes a small change in data structure yields tremendous performance gains.</li><li><strong>Phased rollouts reduce risk </strong>: Gradually implementing changes allows for validation without widespread impact.</li></ol><h3>Conclusion :</h3><p>A small but strategic change adding a simple bucket for partitioning made a huge impact on our platform’s performance and scalability. This change helped us avoid expensive vertical scaling and ensured a smoother, faster experience for millions of users during high-stakes events.</p><p>The improvement didn’t just enhance technical metrics; it directly impacted user experience by ensuring faster contest leaderboard generation and more reliable service. As we continue to grow, this optimization will enable us to scale efficiently while maintaining the performance our users expect.</p><p>It’s a great reminder that sometimes, the simplest solutions bring the biggest results.</p><h3>About author :</h3><p><a href="http://www.linkedin.com/in/mayank-sisode">Mayank Sisode</a> is working as a Software Engineer at Games24x7, with around 4.5 years of experience in backend development. He enjoys solving challenging problems and loves working on distributed systems that handle scale and complexity.</p><h3>References :</h3><ol><li><a href="https://www.instaclustr.com/blog/cassandra-data-partitioning/">https://www.instaclustr.com/blog/cassandra-data-partitioning/</a></li><li><a href="https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/cassandraAbout.html">https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/cassandraAbout.html</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b3a25b1c5c1c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Virtual Threads: Fast, Furious, and Sometimes Stuck]]></title>
            <link>https://medium.com/@Games24x7Tech/virtual-threads-fast-furious-and-sometimes-stuck-654cde6c0c8c?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/654cde6c0c8c</guid>
            <category><![CDATA[computer-science]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[scale]]></category>
            <category><![CDATA[performance]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Tue, 22 Apr 2025 12:52:47 GMT</pubDate>
            <atom:updated>2025-04-22T12:52:47.025Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xAgOJ2nFA2ZNWe_z" /></figure><p>At Games24x7, we handle an extremely high throughput during peak fantasy seasons. Most of our user-path services are built in Java, and to maximize CPU utilization at scale, we rely on reactive frameworks for efficient resource management. However, writing reactive code requires specialized expertise, as it follows a fully asynchronous programming model, unlike traditional Java.</p><p>In this blog, you’ll gain a practical understanding of Java 21’s virtual threads and how they behave in real-world, high-throughput systems. We’ll walk you through a production issue we faced — where our service became unresponsive due to thread pinning caused by synchronized blocks — and how we investigated, reproduced, and ultimately resolved it.</p><p><strong>By the end of this blog, you’ll learn:</strong></p><ul><li>⚙️ <strong>How virtual threads differ from platform threads</strong>, and why they’re game-changing for I/O-heavy applications</li><li>🧵 <strong>What thread pinning is</strong>, why it happens with synchronized blocks, and how it affects virtual thread performance</li><li>🔄 <strong>How ReentrantLock helps avoid thread pinning</strong>, and when to prefer it over synchronized in a virtual thread context</li><li>🔍 <strong>How to debug virtual thread issues using Java Flight Recorder and thread dumps</strong></li><li>🧪 <strong>How to reproduce and simulate thread pinning issues</strong> to validate assumptions before making architectural changes</li><li>🚫 <strong>Common pitfalls when migrating to virtual threads</strong>, and how to avoid them in production</li><li>✅ <strong>Actionable best practices</strong> for safely adopting virtual threads in high-concurrency environments</li></ul><p>Let’s get going -</p><h3>What Are Virtual Threads?</h3><p>Virtual threads are <strong>lightweight</strong> threads managed and scheduled entirely by the JVM. Unlike traditional Java threads, which have a <strong>1:1 mapping to OS threads</strong> and rely on the operating system for scheduling, virtual threads are <strong>decoupled from OS threads</strong>, allowing the JVM to efficiently manage them.</p><p>This design enables <strong>optimized thread scheduling</strong>, significantly improving resource utilization and <strong>allowing thousands or even millions of virtual threads</strong> to run concurrently with minimal overhead</p><h3>How Virtual Threads Enhance Performance and Scalability</h3><p>Virtual threads are mounted on traditional platform threads (carrier threads). They are designed to yield control to the scheduler when they encounter blocking operations, such as I/O waits. When a virtual thread blocks, the JVM unmounts it from its carrier thread, freeing up the carrier thread to handle other tasks. This efficient scheduling mechanism ensures better resource utilization and scalability.</p><p>While virtual threads offer significant performance benefits, they have certain caveats. We encountered one such challenge in our production systems, which we will explore in the following sections, along with our investigation and resolution.</p><h3>Facing the Unexpected</h3><p>The service runs on Java 21 with virtual threads enabled, utilizing 4 CPUs and 12GB of memory per pod.</p><p>In our fantasy app, a <strong><em>match</em></strong> refers to a real-world cricket (or other sport) game in which users participate by creating fantasy teams. During peak match times — especially around the match start — we observe a traffic surge from 2,000 RPS to around 10,000–13,000 RPS for this service. This increase, nearly 5x, can occur within just 2–3 minutes, and in some cases, within as little as 30–45 seconds. The elevated traffic persists as users engage with live features and real-time updates.</p><p>Although the service was designed to handle such load, we observed that some pods became unresponsive during these spikes, failing to serve requests — including health checks and Prometheus metrics APIs. Eventually, these pods were restarted after three consecutive health check failures, in accordance with the liveness probe configuration.</p><p>With no working endpoints during this period, we had no visibility into what was happening, making the issue even more challenging to diagnose.</p><h3>Tracking the Culprit</h3><p>We were able to reproduce the issue in our load-testing environment using a similar load pattern and identified one API that was experiencing high latency.</p><p>This API fetched data from a local cache (LoadingCache from the Caffeine library) while also executing its business logic. At this point, our investigation focused on the loading cache.</p><p>Below is the configuration of our LoadingCache:</p><pre>LoadingCache&lt;Long, Set&lt;Long&gt;&gt; idVsCtSetLoadingCache;<br>private void initLoadingCache(){<br>   idVsCtSetLoadingCache = Caffeine.newBuilder()<br>           .expireAfterWrite(30,TimeUnit.SECONDS) // expire strategy<br>           .maximumSize(100)<br>           .build( id -&gt;{<br>               try {<br>                   return makeAnIO(id); // Rest call to another service<br>               } catch (Exception e) {<br>                   LOGGER.error(&quot;Exception in initLoadingCache loader&quot;, e);<br>                   throw e;<br>               }<br>           });<br>}</pre><p>Since we were performing I/O operations, we suspected that latency in I/O was causing requests to time out. To test this hypothesis, we modified the cache expiration strategy to <strong>RefreshAfterWrite</strong>.</p><p>To our surprise, the initial requests still failed, but once the cache was populated, the issue disappeared. This was our first breakthrough — <strong>LoadingCache was the culprit. But what made it so special?</strong></p><h3>What’s Special -</h3><p>As we dug deeper into virtual threads, we discovered a crucial detail — <strong>they can cause thread pinning when a synchronized block is used</strong>. We will explore thread pinning in more detail later, but this finding was a turning point in our investigation.</p><p>Although we weren’t explicitly using synchronized in our application code, we suspected <strong>LoadingCache</strong>. A closer look at how <strong>LoadingCache’s get() method</strong> works revealed that it relies on <strong>synchronized blocks</strong> in multiple places — especially when the cache is empty, being populated, or when an entry expires (with expireAfterWrite configured)</p><p>Below is the relevant code inside LoadingCache.get(), which internally calls <strong>ConcurrentHashMap’s compute() function</strong>::</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3merWcpTZdrs0qMn" /></figure><p>With this, we were confident that <strong>thread pinning was our issue</strong>, and we needed to validate it with a simple test.</p><p>As mentioned earlier, virtual threads are designed to be <strong>unmounted from platform threads</strong> when they encounter blocking I/O, allowing the platform thread to continue handling other tasks. However, <strong>if a virtual thread is blocked inside a synchronized block, it gets pinned</strong>, preventing the JVM from efficiently scheduling other virtual threads. <strong>This was precisely the issue we encountered</strong></p><h3>Understanding Thread Pinning</h3><p>Thread pinning in Java Virtual Threads occurs when a <strong>virtual thread is bound to a platform thread</strong> for an extended period, preventing the JVM from efficiently scheduling other virtual threads.</p><p>However, problems arise when a virtual thread encounters a <strong>blocking operation that does not yield control back to the JVM</strong>, such as:</p><ul><li><strong>Native synchronization mechanisms</strong> like synchronized blocks.</li><li><strong>Blocking I/O operations</strong> such as Synchronous File I/Os, JDBC calls and socket I/Os.</li></ul><p>When this happens, the virtual thread gets <strong>pinned</strong> to the underlying platform thread. Since the platform thread is now occupied, the JVM <strong>cannot reuse it</strong> to execute other virtual threads, limiting the benefits of virtual threading.</p><h3>But One Pinned Thread Shouldn’t Be a Big Deal, Right?</h3><p>At first, this raised a question for us: <strong>even if one platform thread is pinned, shouldn’t the other platform threads still handle the load?</strong> After all, a few pinned threads shouldn’t bring the entire system down.</p><p>This made us <strong>dig deeper</strong> into what was happening under the hood during thread pinning and why <strong>this issue doesn’t occur in traditional Java applications using OS threads</strong>.</p><p>In the next sections, we’ll uncover the <strong>real reason</strong> why our service became unresponsive and how virtual threads behave in such scenarios. <strong>Stay with us!</strong></p><h3>Simulating the issue</h3><p>The first step in our investigation was to <strong>reproduce the issue</strong> in a controlled local environment using a simple Spring Boot application.</p><p>We designed an application with <strong>two API endpoints</strong>:</p><p>1️⃣ <strong>Business Logic API</strong> — This endpoint simulates real-world processing by executing some business logic and then <strong>sleeping for 20 seconds inside a synchronized block</strong>.</p><p>2️⃣ <strong>Health Check API</strong> — A lightweight endpoint that simply returns 200 OK, mimicking real health checks used in production.</p><p>By running this setup under load, we aimed to observe how virtual threads behave when blocked inside a synchronized block, helping us confirm our suspicions about <strong>thread pinning.</strong></p><p>Application.yaml settings for enabling virtual threads</p><pre>spring:<br> application:<br>   name: caffiene-load<br> threads:<br>   virtual:<br>     enabled: &quot;true&quot;</pre><p><strong>Business Logic API-</strong></p><pre>public List&lt;UserDetails&gt; getUsersForMatch(long matchId) throws ExecutionException, InterruptedException {<br>   List&lt;UserDetails&gt; userList = new ArrayList&lt;&gt;();<br>   synchronized (LOCK){<br>       for(long i=0; i&lt; 100; i++){<br>           UserDetails userDetails = new UserDetails(i, &quot;user&quot;+matchId, &quot;shell&quot;);<br>           userList.add(userDetails);<br>       }<br>       Thread.sleep(20000);<br>   }<br>   return userList;<br>}</pre><p><strong>Health Check API</strong> -</p><pre>public ResponseEntity&lt;?&gt; health() {<br>   try {<br>       Thread.sleep(3000);// 3s sleep to track the thread in JFR for longer duration.<br>   } catch (InterruptedException e) {<br>       throw new RuntimeException(e);<br>   }<br>   return ResponseEntity.ok(Thread.currentThread().getName());<br>}</pre><p><strong>Dockerfile -</strong></p><p>To get deeper insights into what was happening at the system level, we <strong>containerized</strong> our test application by creating a <strong>Docker image</strong>.</p><p>Inside the container, we installed:<br> <strong>strace</strong> — To trace system calls made by the application.<br> <strong>procps</strong> — To monitor process and CPU usage in real time.</p><pre>FROM openjdk:21-slim<br>EXPOSE 8080<br><br>RUN apt update &amp;&amp; apt install procps -y<br>RUN apt install strace -y<br>ADD target/test-local-cache-load-1.0-SNAPSHOT.jar /home/tomcat/test-local-cache-load-1.0-SNAPSHOT.jar<br><br>ENTRYPOINT [&quot;bin/bash&quot;,&quot;-c&quot;]<br>CMD [&quot;java -jar /home/tomcat/test-local-cache-load-1.0-SNAPSHOT.jar&quot;]</pre><p>docker-compose.yaml to simulate a pod with restricted resources and take thread dumps and JFR (Java Flight Recorder) recordings.</p><pre>version: &#39;3.8&#39;<br><br>services:<br> app:<br>   image: caffiene-load:latest<br>   container_name: caffeine-load-test<br>   ports:<br>     - &quot;8080:8080&quot;<br>   environment:<br>     - JAVA_OPTS=-Xms2g -Xmx2g<br>   deploy:<br>     resources:<br>       limits:<br>         cpus: &quot;2.0&quot;<br>         memory: &quot;3g&quot;<br>       reservations:<br>         cpus: &quot;2.0&quot;<br>         memory: &quot;3g&quot;<br>   volumes:<br>     - /Users/ssg/ssg/thread-dumps:/home/tomcat/thread-dumps</pre><p>To replicate the issue more closely, we <strong>restricted our test environment to 2 CPU cores</strong> and followed these steps:</p><ol><li><strong>Started the application.</strong></li><li><strong>Called the main API twice</strong>, manually hitting it <strong>sequentially</strong> with a <strong>1–2 second interval.</strong></li><li><strong>Called the health check API</strong> immediately after step 2.</li></ol><p><strong>Observation:</strong></p><p>The <strong>health check API </strong>was blocked until the first API call <strong>completed (20 seconds)</strong> and the thread was freed. This meant that <strong>no other requests were processed during this period</strong>, including the health check!</p><p>This was a clear indication that our platform threads were getting <strong>pinned</strong>, preventing the JVM from scheduling other virtual threads.</p><p>Let’s break down why this happened</p><h3>What Are My Threads Doing?</h3><p>To get deeper insights into thread behavior, we started <strong>Java Flight Recorder (JFR) for 200 seconds</strong> inside the Docker container and <strong>re-ran our test scenario</strong> from the previous section.</p><p>You can use the following command to <strong>capture a JFR recording</strong> using jcmd:</p><pre>jcmd &lt;java pid&gt; JFR.start name=myRec filename=&lt;filepath&gt;/&lt;filename&gt;.jfr duration=200s</pre><p>After 200 seconds, we ran the JDK mission control tool to analyse the JFR recording, and we found the following observations -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JRCgfAjfweOE5KGY" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZTmLOf9jGUzOncUU" /></figure><p>​​From our <strong>first JFR image</strong>, we identified the <strong>pinned threads</strong>. Let’s break down what we observed:</p><ol><li>The first thread (<strong>tomcat-handler-0</strong>) entered the <strong>synchronized block</strong> at <strong>09:07:23</strong> and <strong>slept for 20 seconds</strong>. It was freed only at <strong>09:07:43</strong>.</li><li>As soon as the first thread(<strong>tomcat-handler-0)</strong> exited the synchronized block, the <strong>second thread (tomcat-handler-1)</strong> acquired the lock and got pinned in the same manner</li></ol><p>At this point, we had two important questions:</p><ol><li><strong>Why were there only 2 platform threads (ForkJoinPool) and not more?</strong></li><li><strong>If both platform threads were getting pinned sequentially, why didn’t the waiting health check API execute on another thread?</strong></li></ol><h3>Answering These Questions</h3><h4>1) Why Only 2 Platform Threads?</h4><p>Since virtual threads are entirely managed by the<strong> </strong>JVM, the number of platform threads is determined based on available CPU cores.</p><ul><li>In our Docker container, we restricted CPUs to 2, so the JVM allocated only 2 platform threads for the ForkJoinPool.</li><li>This means only two virtual threads could be scheduled at any given time, making thread pinning a <strong>critical bottleneck</strong> in this case</li></ul><p><strong>2)</strong> <strong>Why Didn’t the Health Check API Execute?</strong></p><p>To answer this, let’s look at the second JFR image, which shows a blocked Java monitor (i.e., a blocked thread):</p><ul><li>The second thread (tomcat-handler-1) was waiting to acquire the lock on the synchronized block and was completely blocked.</li><li>Because this thread was blocked, the JVM did not unmount the virtual thread from the platform thread.</li><li>This meant that both available platform threads were pinned, leaving no free threads to execute the health check</li></ul><p><strong>The result</strong>? A complete service freeze.</p><p>This was our <strong>End of the World</strong> moment — both platform threads were stuck, leading to service unresponsiveness.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/220/0*nAL4VtRjgxY0MRqe" /></figure><blockquote>If we run the same code with virtual threads disabled and provide tomcat max threads to be more than 2 then we can see that this issue doesn’t come because a new platform thread is created once the earlier 2 threads are blocked on synchronized block (1st inside the block and 2nd waiting for the lock). The health check request is executed on a 3rd thread. Here also, if we limit the Tomcat max threads to 2, then we can also see a similar issue that the health check api is not executed, as both the threads are blocked on a synchronized block.</blockquote><p>Let’s dive deeper into the second point to understand why the thread waiting on the synchronized block remains blocked</p><h3>Demystifying The World of Virtual Threads</h3><p>Based on our discussion, we’ve pinpointed the issue to the synchronized block — specifically, how the platform thread behaves once it enters the block and how the virtual threads waiting for the lock are affected.</p><p>As per Java documentation -</p><blockquote>When a virtual thread is inside a synchronized instance method and invokes Object.wait() on the object, the virtual thread blocks in the JVM until awakened with Object.notify() and the carrier re-acquires the monitor. The virtual thread is pinned. It is executing inside a synchronized method, and further pinned because its carrier is blocked in the JVM.</blockquote><p>This meant that both our platform threads from the previous discussion were completely occupied. Our curiosity kicked in, and we couldn’t resist diving deeper. So, we set out to prove it ourselves — let’s uncover what’s happening at the OS level</p><h3>What Is Happening At The OS Level?</h3><p>We now understand that when platform threads are blocked, the JVM cannot process new requests or perform other tasks using the fork-join pool until those threads are released. Since each platform thread is mapped 1:1 to an OS thread, this also implies that the corresponding OS thread is blocked as well.</p><p>To validate this hypothesis, we decided to trace all <strong>futex</strong> (fast user-space mutex) syscalls at the OS level using <strong>strace</strong>. To capture futex activity, we ran the following commands</p><pre>nohup strace -f -tt -p &lt;Java Pid&gt; -e futex &gt; strace.out 2&gt;&amp;1 &amp;<br>ps -T -p &lt;Java Pid&gt;</pre><p>The first command traces all <strong>futex</strong> syscalls made by the Java application at the OS level. The second command helps identify the <strong>PIDs of fork-join pool threads</strong> — these threads are created only when an API is hit or a task is running on them. So, to capture the correct data, we first hit the APIs and then ran the second command to get the PIDs</p><p>In our case, after hitting two APIs followed by the health check API, we checked the fork-join pool PIDs using the second command. We observed that both fork-join pool threads were created. Next, we filtered the <strong>futex</strong> traces specifically for these threads, and here’s what we found.</p><p><strong>Thread with the first request</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*myks3Rp1mP7nWcKW" /></figure><p>This is the 1st thread that takes the lock and sleeps for 20 seconds in the synchronized block</p><p><strong>Thread with the 2nd request</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*G0LYx6suvTdxiirV" /></figure><p>The above image shows the other thread, which tries to take the lock, and that goes on for 20 seconds, and every request is timing out as the lock is not available. This is all happening at the OS level, which shows that the control is now with the OS.</p><h3>The Saviour — Reentrant Lock</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/220/0*nte4FVuKhGm50ngL" /></figure><p>So far, we’ve discussed how the <strong>synchronized block</strong> leads to <strong>thread pinning</strong>, causing waiting threads to be blocked. To avoid this, Java documentation suggests using <strong>ReentrantLock</strong> instead.</p><p>Naturally, we wanted to test this ourselves. We modified our previous method, replacing the synchronized block with <strong>ReentrantLock</strong>. Below is the updated method</p><pre>private ReentrantLock reLock = new ReentrantLock();<br>public List&lt;UserDetails&gt; getUsersForMatch(long matchId) throws Exception {<br>   List&lt;UserDetails&gt; userList = new ArrayList&lt;&gt;();<br>   try{<br>       reLock.lock();<br>       for(long i=0; i &lt; 100; i++){<br>           UserDetails userDetails = new UserDetails(i, &quot;user&quot;+matchId, &quot;shell&quot;);<br>           userList.add(userDetails);<br>       }<br>       Thread.sleep(20000);<br>   } finally {<br>       reLock.unlock();<br>   }<br>   return userSet;<br>}</pre><p>After replacing the synchronized block with ReentrantLock, we repeated the same sequence of steps and generated a new JFR (Java Flight Recorder) file. Here’s what we found</p><ul><li><strong>Health Check API Execution:</strong><br>- Unlike before, the health check API was executed immediately and was not blocked.<br>- Initially, we assumed that the first thread might still be blocked inside the lock, but the waiting thread was free and handled the health check request.</li><li><strong>Fork-Join Pool Behavior:</strong><br>- We triggered the Business logic API twice sequentially, meaning one request was inside the lock, and the second request was waiting.<br>- Immediately after, we checked the fork-join pool PIDs using: <strong>ps -T -p &lt;Java Pid&gt;<br>- </strong>To our surprise, only one active thread was created!<br>- The second thread was not even spawned in the fork-join pool.</li></ul><p>This was an unexpected but crucial finding. Instead of pinning and blocking additional threads, ReentrantLock prevented unnecessary thread creation, ensuring efficient thread management. But why did this happen? Let’s dive deeper.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LCgtZnkov5SiouDm" /></figure><p>Let’s break down the last observation -</p><ul><li><strong>First API Call Execution:</strong><br>- A platform thread from the fork-join pool is assigned to a virtual thread.<br>- The virtual thread enters the ReentrantLock and sleeps for 20 seconds.</li><li><strong>Second API Call Execution:</strong><br>- When the second request arrives, the same platform thread is reused.<br>- This happens because the virtual thread inside the ReentrantLock(reLock) gets unmounted, freeing the platform thread.<br>- The new request gets assigned to a new virtual thread, which runs on the same freed platform thread.</li><li><strong>Health Check API Execution:</strong><br>- When we hit the health check API, the same freed platform thread is used to process the request because, unlike the synchronized block, where a waiting thread remains blocked, here the waiting virtual thread unmounts, keeping the platform thread free for other tasks.</li><li><strong>Final State (After Sleep Completion):</strong><br>- Once the first request’s sleep completes, its virtual thread needs a platform thread to resume execution.<br>- Similarly, the second virtual thread, which was waiting for the lock, now acquires the lock and also needs a platform thread.<br>- At this point, two platform threads are required to complete both pending tasks.<br>- To verify this, we ran the ps -T -p &lt;Java Pid&gt; command as soon as the first request completed.<br>- We observed that two fork-join pool threads were now active, confirming that platform threads are efficiently reused rather than being blocked.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Cfft7I0t5dXSY9JH" /></figure><p>The above observations are exactly what is expected from a virtual thread’s behaviour to make the system more efficient and performant. Below is the image showing no pinning and no blocked threads in the JDK mission control tool for the JFR file -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*AtuJkp_PKkLnrM6P" /></figure><p>So now the question arises, what’s the difference between a synchronized block and a reentrant lock? Let’s understand -</p><h3>Why Does Unmounting Happen in ReentrantLock</h3><p>The major difference between a synchronized block and reentrant lock is that the in case of synchronized block the control is handed over to the OS by JVM using syscalls and JVM doesn’t have the complete control over that (also called the intrinsic lock), while in case of reentrant lock the lock is managed by the JVM completely.</p><p>So when a virtual thread is waiting for reentrant lock or blocked inside a reentrant lock the JVM allows it to unmount as JVM has all the control, while in case of synchronized block the lock is managed by the OS and since JVM doesn’t have the control over it it doesn’t allow the unmounting and the platform thread is stuck.</p><p>To better understand this, we can visualize the difference through animations. The <strong>black arrows represent the carrier threads,</strong> and the <strong>green ones are the virtual threads</strong>. This animation is for <strong>2 CPUs with sequential calls</strong> to the application as discussed earlier in the blog</p><p><strong>Thread Flow With Synchronized block</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*hNZHOY9FlkxgOf0n" /></figure><p>Here, we can see that both platform threads — one inside the synchronized block (pinned) and the other waiting for the lock (blocked) — are fully occupied. As a result, the system stalls, and no new requests are accepted since only two platform threads are available. This occurs because a synchronized block acquires a lock at the OS level, where the <strong>OS manages the lock instead of the JVM</strong>, leaving the JVM unable to intervene.</p><p><strong>Thread Flow With Reentrant Lock</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*QrJcPDc2DwmVYIdA" /></figure><p>Here we can observe that when the virtual thread is blocked, the platform thread is unmounted and the same platform thread is reused for different requests. This happens because the reentrant lock is managed by the JVM, and the JVM has all the control over it</p><h3>Key Takeaways</h3><ul><li>Minimize the use of synchronized blocks when working with virtual threads. If synchronization is necessary, ensure that no I/O or blocking operations are performed inside the block.</li><li>Prefer ReentrantLock over synchronized for better scalability and performance, as it allows the JVM to manage locks efficiently without causing thread pinning.</li><li>Handle library-induced synchronization carefully<strong>.</strong> If a synchronized block comes from a library, such as Caffeine’s LoadingCache in our case, opt for its async variant to avoid blocking. If no async variant is available, consider switching to a library that is either lock-free or supports asynchronous execution.</li><li>Here are some common library alternatives to avoid thread pinning and achieve better scalability:</li><li>Switch from JDBC to R2DBC, which is completely non-blocking.</li><li>If you are using Apache HttpClient or OkHttp, they can cause thread pinning; instead, switch to java.net.http.HttpClient, which is non-blocking internally.</li><li>Use AsyncLoadingCache instead of LoadingCache, as it provides non-blocking behavior.</li></ul><h3>Conclusion</h3><p>Virtual threads have revolutionized Java by enabling massive scalability, allowing thousands to millions of lightweight threads to run efficiently while reducing the overhead of traditional OS threads. We’ve successfully optimized multiple production applications using Java 21, but an unexpected load pattern combined with a simple synchronized block led to significant performance degradation.</p><p>This challenge provided invaluable insights into debugging and fine-tuning our application for virtual threads. Fortunately, Java 24 has addressed this issue for the synchronized block, making virtual threads even more robust and reliable for high-performance applications.</p><h3>About the Authors</h3><p><a href="https://www.linkedin.com/in/saurabhsingh25/">Saurabh Singh</a> is a senior backend engineer at Games24x7 with over 6 years of experience in software development. He specializes in building and optimizing high-scale distributed applications, focusing on enhancing performance while reducing costs.</p><p><a href="https://www.linkedin.com/in/jatin-sinha-34899817a/">Jatin Sinha</a> is working as SDE-2 at Games24x7 and has around 4.5 years of experience. He likes problem-solving and enjoys building and maintaining distributed systems at scale.</p><h3>Resources -</h3><ol><li>Virtual Threads — <a href="https://openjdk.org/jeps/425">https://openjdk.org/jeps/425</a></li><li>Synchronize Virtual Threads without Pinning<strong> </strong>— <a href="https://openjdk.org/jeps/491">https://openjdk.org/jeps/491</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=654cde6c0c8c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mastering Operational Excellence: Preventing and Managing Outages Effectively]]></title>
            <link>https://medium.com/@Games24x7Tech/mastering-operational-excellence-preventing-and-managing-outages-effectively-17dc3e12de37?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/17dc3e12de37</guid>
            <category><![CDATA[outage]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[monitoring]]></category>
            <category><![CDATA[operations]]></category>
            <category><![CDATA[excellence]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Thu, 20 Mar 2025 05:32:38 GMT</pubDate>
            <atom:updated>2025-03-20T05:32:38.170Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/719/0*SY_AtXPSR-EHhe4I" /></figure><p>Production outages in popular online game platforms like My11Circle and RummyCircle can create a huge dent in revenues and cause reputation losses, especially in peak season. We, at Games24x7, aim to ensure at least 99.98% availability of our critical systems. This means that we cannot have an accumulated downtime of more than 1 hour 45 minutes across all our products within the year. While this is achieved by building resilient, fault-tolerant, and highly available systems, at the same time there is a huge role in setting up robust processes that can help in the prevention of production issues, reduction of time to detect and fix issues, and making sure we learn from the past outages. This blog will try to give you insights into the many engineering and operational practices followed at Games24x7 to keep the outages at bay.</p><h3><strong>Prevention:</strong></h3><ol><li><strong>Having the right attitude:</strong> Too much has been written online on best practices to prevent production issues, but mainly it is the mindset that has a big role to play here. At Games24x7, we try to inculcate a sense of ownership in the mind of every engineer. In different forums, leaders often talk about how exponentially we have grown over the years and are still growing. With such a large user base, even one minute of downtime can be extremely detrimental to the business.</li><li><strong>Effective engineering methods:</strong> Our engineers are trained to always optimize for scale and solve for resiliency in every piece of code they write or test. The developers make sure critical flows are not impacted by auxiliary functionalities by appropriate error handling or having separate thread pools. We also have infra-level segregation of different sets of components to keep things isolated and independent. We encourage having on-the-fly configurable switches like feature/functionality level toggles, bypassing slave DBs, etc. This coupled with some other good practices like stringent code reviews, test case walkthroughs, cross-team architecture review forums, minimum 80% unit test coverage, Functional Testing Automation, Performance Tests, User Acceptance Tests, Regression Tests, Canary Deployments, Deployment plan approvals, etc helps us avoid most of the functional and non-functional issues going to production.</li><li><strong>Planning for major events</strong>: To take it a step further in terms of better preparation before major events, we do dry runs on production. This <a href="https://medium.com/p/efb197b9ace8">blog</a> by one of my colleagues can help you deep dive into this philosophy of dry runs. In our fantasy platform My11Circle where we see sudden traffic spikes, we don’t rely on the autoscaling of ASGs or HPA alone. Instead, we have built our own Data Science powered scaling <a href="https://medium.com/@Games24x7Tech/automated-infrastructure-scaling-using-ml-284ab7113c9e">system</a>, which can predict the traffic based on the type of the event and current liquidity and pre-scale our systems accordingly.</li></ol><h3><strong>Monitoring and alerting:</strong></h3><p>Having an exhaustive monitoring and alerting system in place is extremely crucial. In Games24x7, we monitor different dimensions of our systems:</p><ol><li><strong>Infrastructure metrics</strong>: This mainly covers the usage of resources like CPU, Memory, Disk space, process crashes, etc. Tools used: AWS Cloudwatch, Prometheus, Grafana.</li><li><strong>Application metrics</strong>: This covers the throughput and latencies of different APIs and event consumers of our microservices. We also monitor the different types of error codes emitted from our load balancers and target groups. Tools used: AWS Cloudwatch, Prometheus, StatsD, Graphite, Grafana.</li><li><strong>Exceptions/Errors</strong>: This covers the exceptions or errors occurring in your application code. Tools used: Overops, Harness, Glitchtip, Sentry.</li><li><strong>Critical Business Metrics</strong>: We also plot our critical business metrics like user logins, user registrations, game joins, etc from our microservices. We also detect abnormal trends in traffic patterns using tools like Anodot and our in-house <a href="https://medium.com/@Games24x7Tech/enhancing-grafana-alerting-with-dynamic-thresholds-d2a5021c4604">Dynamic monitoring tool</a>.</li><li><strong>Centralized logs dashboard</strong>: We use Last9 which ingests logs from different sources and enables our engineers to search for patterns in the logs for better and quicker debugging.</li></ol><p>Most of the above metrics also have corresponding alerts in place and these alerts are integrated with Pagerduty for critical alerts, and with Slack/Email for warnings.</p><h3><strong>Nipping the issue in the bud:</strong></h3><p>It is extremely crucial to not overlook any symptoms and give every alert its due, so that a small issue doesn’t snowball into an outage. We have classified the production issues into different categories and defined different Standard Operating Procedures for tackling each of them:</p><ol><li><strong>Email/Slack alerts</strong>: These are the non-critical types of alerts that are not urgent but can be signs of an upcoming major issue or can hamper the experience for some users if not resolved in time. The on-call engineer is supposed to evaluate the alert, do the impact analysis, and create a Jira ticket with the right severity. The severity can help in prioritizing the ticket appropriately.</li><li><strong>PagerDuty Alerts</strong>: These are the alerts that mostly indicate something is wrong with production and needs to be addressed before it gets converted into an outage. These alerts again go to the On-call engineer who has to create a Jira ticket of a higher priority and start the investigation and mitigation. There are run-books in place for common known issues which have happened in the past and are handy to the engineer in our confluence workspace.</li><li><strong>Production Issues raised by users</strong>: These are the Jira tickets that are either raised by our customer support team on the complaints of our end-users or by our business teams. These issues indicate that a set of users are facing a bad experience on the app. The on-call engineer gathers various inputs from the customer support/business teams to gauge the blast radius of the issue, and the functionality that is impacted and accordingly assigns a severity to the Jira ticket. There is an SLA defined for every severity for mitigation and permanent fix.</li><li><strong>Outages</strong>: These are the issues that impact a large number of end users and/or impact the revenue numbers adversely. A Jira ticket is raised on every outage, and with the help of Jira integration with PagerDuty, an alert is generated for all the tech leaders. A Slack channel with a Zoom link and the Jira ticket details is automatically created. The entire team jumps onto the Zoom call to address the outage without any delay. The focus is more on mitigation rather than on permanent fixes in such scenarios. Once the issue is mitigated, then the next natural step is a detailed RCA and a plan for a permanent fix.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*y0k653ODK_raS29W" /></figure><h3><strong>Alert Dashboards and Reviews:</strong></h3><p>As you might have observed in the previous section, every production issue needs to have a corresponding Jira ticket for tracking. Each Jira ticket has an owner and an ETA as per the severity. These Jira tickets are grouped into different Jira Projects and dashboards are created out of those projects. The tech leadership does a weekly review of all these dashboards to make sure that things are moving as per the SLAs and that any delays/blockers are called out. These reviews help us to keep ourselves accountable. The review meeting is also a forum to discuss the Correction of Errors (COE) which I will cover in the next section.</p><h3><strong>Learning from the mistakes:</strong></h3><p>Mistakes are part and parcel of our lives, but learning from them and not repeating them is what makes us better from our past selves. Every outage or rollback that happens on production needs to have a COE (Correction of Errors) document. This document has all the details of the outage, the Root Cause Analysis, the learnings and the action items (again tracked as Jira tickets) so that we improve our systems and don’t repeat the same mistakes. The COE document is supposed to be created as soon as the outage is mitigated. The COE document is reviewed with the tech leaders and architects to ensure it is comprehensive. We follow a standard template for the COE doc so that nothing is left to be answered. The template consists of the following sections:</p><ol><li>Summary</li><li>Customer/Revenue Impact</li><li>Detailed Timeline of the issue and the mitigation</li><li>Questions around the issue discovery:<br>- How was the issue discovered?<br>- How long did it take to know about the issue from the first impact?<br>- Was there a known bug that caused the issue?<br>- Were there existing backlog items for this issue?<br>- Was this a known failure mode?<br>- Was the issue caused by deployment or build release?<br>- Was it caused by manual deployment or a configuration change?<br>- As a thought experiment what could be done to reduce the time to detection by half?</li><li>Learnings</li><li>Corrective actions</li></ol><h3><strong>Closing Note:</strong></h3><p>In conclusion, building a resilient team capable of preventing and efficiently responding to outages is not just a technical necessity; it’s also a cultural commitment. At Games24x7, we recognize that our success hinges on our ability to maintain an exceptional user experience, especially during critical times. By fostering a mindset of ownership and accountability among our engineers, implementing robust monitoring systems, and learning from every incident, we continuously improve our processes. With a focus on prevention and proactive engagement, we aim to uphold our promise of 99.98% system availability, ensuring that our platforms remain reliable and enjoyable for all users. Together, we can turn challenges into opportunities for growth, solidifying our reputation as leaders in the online gaming industry.</p><h3>Author Info</h3><p><a href="https://www.linkedin.com/in/dhiraj-prajapati-0482231b/">Dhiraj Prajapati</a> is a Senior Engineering Manager at Games24x7, currently building their fantasy sports platform My11Circle. He has been working in the software engineering space for the last 13 years, mainly focussing on distributed systems running at a high scale.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=17dc3e12de37" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Optimally Grouping cards in Rummy — Tips, Tricks and an Algorithmic approach with Code]]></title>
            <link>https://medium.com/@Games24x7Tech/optimally-grouping-cards-in-rummy-tips-tricks-and-an-algorithmic-approach-with-code-0ecd44793251?source=rss-8e0c59a39d31------2</link>
            <guid isPermaLink="false">https://medium.com/p/0ecd44793251</guid>
            <category><![CDATA[tips-and-tricks]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[dynamic-programming]]></category>
            <category><![CDATA[data-structures]]></category>
            <category><![CDATA[rummy]]></category>
            <dc:creator><![CDATA[Games24x7 Blogs]]></dc:creator>
            <pubDate>Fri, 17 Jan 2025 09:44:29 GMT</pubDate>
            <atom:updated>2025-01-17T09:44:29.720Z</atom:updated>
            <content:encoded><![CDATA[<h3>Optimally Grouping cards in Rummy — Tips, Tricks and an Algorithmic approach with Code</h3><h3>All About Indian Rummy</h3><p>Rummy is a card game played with two decks of cards, including the two Jokers. To win the game, a player must make a valid declaration by drawing and discarding cards from two piles: a closed deck, where the player cannot see the card they are picking, and an open deck, which consists of cards discarded by players. To win, players need to arrange their cards into valid sequences and sets.</p><p>We will discuss in detail on how to maximize wins and minimize losses in the game of Rummy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X4hTnNDrVmst1jjA.png" /></figure><p>Example of a valid declaration of a Rummy Game on rummycircle.com</p><h3>Terminologies</h3><ul><li><strong>Card</strong>: A playing card with a rank (1–13) where 11–13 are face cards (J, K, Q) and a suit (♣,◆, ♥, ♠).</li><li><strong>Printed Joker Card</strong>: Two special cards apart from 52 cards of the deck that can substitute for any card in a player’s hand.</li><li><strong>Wild Card</strong>: A special card drawn at the start of the game that acts similar to the printed joker and can substitute for any card in a player’s hand.</li><li><strong>Pure Sequence (PS)</strong>: A set of 3 and more cards with consecutive ranks and the same suit. For example, (4♥, 5♥, 6♥, 7♥, 8♥) is a Pure Sequence of length 5, featuring consecutive ranks (4, 5, 6, 7, 8) and the suit ♥.</li><li><strong>Impure Sequence/Sequence (IS or Seq)</strong>: A set of 3 or more cards with consecutive ranks and the same suit, including one or more Joker cards. For instance, (J♥, 5♣, K♥, A♥) is an Impure Sequence of length 4, with 5♣ serving as the Joker card.</li><li><strong>Set</strong>: A combination of 3 or 4 cards with the same rank but different suits. For example, (J♣, J◆, J♥, J♠) is a Set of 4 cards, all with the rank 11 but with four different suits.</li><li><strong>Invalid</strong>: Any card combination that does not fit into any of the categories above (PS, IS, Set) is considered Invalid.</li><li><strong>Meld</strong>: Any combination of cards that is either a PS, IS, Set, or Invalid.</li><li><strong>Valid Declaration</strong>: A grouping with no Invalid melds and at least 2 Sequences, with at least one being a PS. The remaining melds can be any type (PS, IS, Set).</li><li><strong>Open Deck</strong>: A stack of face-up cards placed on the table from which players can draw during their turns. This pile is visible to all players, enabling them to make strategic decisions based on the cards shown. It is also referred to as the Discard Pile..</li><li><strong>Closed Deck</strong>: A stack of face-down cards remaining after dealing. The cards are arranged randomly and their identities are hidden from the players. Players draw from this deck without knowing which cards they will receive.</li></ul><h3>How to Play Rummy</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FqEixIL5uI6w&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqEixIL5uI6w&amp;image=http%3A%2F%2Fi.ytimg.com%2Fvi%2FqEixIL5uI6w%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4acd1236649fdea63e2a5df6924a4949/href">https://medium.com/media/4acd1236649fdea63e2a5df6924a4949/href</a></iframe><p>Creating valid sequences and sets can be challenging, as the number of combinations increases exponentially with each card dealt to the player, having an upper bound of 13! combinations.</p><p>The Point System in Rummy can be found at <a href="https://www.rummycircle.com/how-to-play-rummy/rummy-points-system.html">Rummy Circle</a>.</p><h3>Tips and Tricks</h3><ol><li>Once the cards are dealt, start grouping them to create both pure and impure sequences; otherwise, it will be more challenging to minimize your points as the game progresses.</li><li>Focus on hands that include at least one joker along with a pure sequence, as this combination often gives you an advantage early in the game, making it easier to reduce your points.</li><li>Keep a close eye on what your opponents are selecting from the open deck, as it can offer insights into their hands and what they’re aiming to form.</li><li>At the start of the game, evaluate the potential for forming pure and impure sequences with your cards. The more cards you have that can help create these sequences, the better your chances of minimizing points.</li><li>Based on your initial grouping, decide whether to drop the hand. Use the strategies discussed to guide your choice on whether to continue playing.</li></ol><p>Therefore, it’s crucial to optimally group your cards with each move to minimize points, which increases your chances of winning and reduces potential losses if you do end up losing.</p><p>More tips and tricks here — <a href="https://www.rummycircle.com/how-to-play-rummy/rummy-tips-tricks.html">https://www.rummycircle.com/how-to-play-rummy/rummy-tips-tricks.html</a></p><h3>Algorithmic approach to Optimal Grouping</h3><p>Since our goal is to combine cards in a way that minimizes points, this becomes a combinatorial problem. It can be effectively solved using Dynamic Programming with Bitmask.</p><h4>Brute Force Approach</h4><p>The brute force solution involves checking all permutations and grouping the cards into lengths of 3 to 5 for pure and impure sequences, and lengths of 3 to 4 for sets. To illustrate, let’s simplify this with an example using 7 cards instead of 13.</p><p><strong>Given hand:</strong> 6♣, 7♣, 8♣, 7♠, 9♠, 10♠, 8♠.</p><p>There are 7! permutations possible. For example:</p><ul><li>(6♣, 7♣, 8♣, 7♠, 9♠, 10♠, 8♠)</li><li>(6♣, 7♣, 7♠, 8♣, 9♠, 8♠, 10♠)</li><li>(6♣, 7♣, 8♣, 7♠, 8♠, 9♠, 10♠), and so on.</li></ul><p>Possible groupings length could be (3, 3, 1), (3, 4) and (5,2). We explain these lengths below.</p><p>Let’s take the last permutation (6♣, 7♣, 8♣, 7♠, 8♠, 9♠, 10♠) and consider the grouping (3, 3, 1):</p><ul><li><strong>Meld 1:</strong> First 3 cards (6♣, 7♣, 8♣) → Points = 0 (pure sequence).</li><li><strong>Meld 2:</strong> Next 3 cards (7♠, 8♠, 9♠) → Points = 0 (pure sequence).</li><li><strong>Meld 3:</strong> Last card (10♠) → Points = 10 (invalid).</li></ul><p>Total points = 0 (Group 1) + 0 (Group 2) + 10 (Group 3) = 10 points.</p><p>Now, using the same permutation (6♣, 7♣, 8♣, 7♠, 8♠, 9♠, 10♠) with the grouping (3, 4):</p><ul><li><strong>Meld 1:</strong> First 3 cards (6♣, 7♣, 8♣) → Points = 0 (pure sequence).</li><li><strong>Meld 2:</strong> Next 4 cards (7♠, 8♠, 9♠, 10♠) → Points = 0 (pure sequence).</li></ul><p>Total points = 0 (Group 1) + 0 (Group 2) = 0, making this an optimal grouping.</p><p>Permutations to check 13! * 8 = 49816166400, taking approximately 1.5 hours. Don’t worry we discuss a solution that runs under 10 milliseconds.</p><h4>Dynamic Programming with Bitmask</h4><p>The brute force approach is impractical for real-time assistance to players due to its complexity. To improve upon this, we can leverage dynamic programming with bit-masking, which helps manage overlapping subproblems and enables efficient memorization.</p><p>Let’s go through the logic along with python code. We provide the helper functions below.</p><p><em>Imports:</em></p><pre>import numpy as np<br>import copy<br>import random<br>import itertools<br>from scipy.ndimage import shift</pre><p><em>Hand points calculation logic:</em></p><pre># locations - store the locations of the card in the hand<br># hand_array - store the hand as a tuple of suit and card<br># hand - store the hand as array of string<br># joker - joker card<br>def calculcate_hs(locations,hand_array,hand,joker):<br>    sc = 0<br>    for loc in locations:<br>        if not (hand_array[loc][0] == -1 or hand_array[loc][1] == int(joker[:-1])):<br>            if hand_array[loc][1] == 1:<br>                sc+=10<br>            else:<br>                sc+=min(10,hand_array[loc][1])<br>    return sc</pre><p><em>Get count of cards already processed:</em></p><pre># get the number of the cards that has been considered in the hand<br>def get_len(num):<br>    s=0<br>    while num!=0:<br>        s+=(num &amp; 1)<br>        num = num&gt;&gt;1<br>    return s</pre><p><em>Get the location of the cards already considered:</em></p><pre># get all the cards position that are considered in the hand<br>def get_set_pos(num):<br>    i = 0<br>    pos = []<br>    while num!=0:<br>        if (num &amp; 1) == 1:<br>            pos.append(i)<br>        num = num&gt;&gt;1<br>        i+=1<br>    return pos</pre><p><em>Make a suit to cards and cards to suit mapping present in the hand:</em></p><pre># get suit to card mapping dictionary both including and excluding the wild card<br>def get_suit_card_mapping(hand0,joker):<br>    different_suits = {&#39;h&#39;:[], &#39;s&#39;:[], &#39;c&#39;:[], &#39;d&#39;:[]}<br>    different_suits_without_joker = {&#39;h&#39;:[], &#39;s&#39;:[], &#39;c&#39;:[], &#39;d&#39;:[]}<br>    for i,card in enumerate(hand0):<br>        initial = card[0]<br>        remaining = int(card[1:])<br>        different_suits[initial].append(remaining)<br>        if card in joker or card == &#39;j&#39;:<br>            continue<br>        else:<br>            different_suits_without_joker[initial].append(remaining)<br>    for card_type in different_suits:<br>        different_suits[card_type] = sorted(different_suits[card_type])<br>        different_suits_without_joker[card_type] = sorted(different_suits_without_joker[card_type])<br>    return different_suits,different_suits_without_joker<br># get card to suit mapping dictionary<br>def get_card_suit_mapping(hand0,joker):<br>    different_suits = {1:[], 2:[], 3:[], 4:[], 5:[], 6:[], 7:[], 8:[], 9:[], 10:[], 11:[], 12:[], 13:[]}<br>    for i,card in enumerate(hand0):<br>        initial = card[0]<br>        remaining = int(card[1:])<br>        if card in joker or card == &#39;j&#39;:<br>            continue<br>        else:<br>            different_suits[remaining].append(initial)<br>    return different_suits</pre><p><em>Store all the hash in relevant meld type — Pure Sequence, Impure Sequence and Sets. ‘typ’ 1 is pure sequence, ‘typ’ 2 is impure sequence and ‘typ’ 3 are sets in the code below:</em></p><pre># add cards which are considered in the hand based on type pure seq/impure seq/set in the good hands which will be part of the optimal grouping<br>def mark_location(comb,loc_mapping,idx,mask,good_hands,typ):<br>    if len(comb) == idx:<br>        good_hands[typ].add(mask)<br>    else:<br>        if type(comb[idx]) == tuple:<br>            pos = 2**comb[idx][1]<br>            mark_location(comb,loc_mapping,idx+1,mask+pos,good_hands,typ)<br>        else:<br>            for i in loc_mapping[comb[idx]]:<br>                pos = 2**i<br>                mark_location(comb,loc_mapping,idx+1,mask+pos,good_hands,typ)</pre><p>Lets Begin,</p><p>First, let’s explore the combinatorial aspect of this problem. Since each player is dealt 13 cards, we can represent each card with a unique bit which behaves as a hash for the card, resulting in ²¹³ = 8192 possible combinations for card selection. For example, if a player has the hand (5♣, 6♣, 7♣, 8♣, 9♣, 7♠, 8♠, 9♠, 10♠, J♥, K♥, A♥, K♦), we can assign bits as follows:</p><ul><li>5♣ is represented by the 0th bit and can be represented as ²⁰ = 1,</li><li>6♣ by the 1st bit and can be represented as ²¹ = 2,</li><li>7♣ by the 2nd bit and can be represented as ²² = 4,</li><li>8♣ by the 3rd bit and can be represented as ²³ = 8,</li><li>9♣ by the 4th bit and can be represented as ²⁴ = 16</li></ul><p>and so on for the remaining cards.</p><p>We have established a method to uniquely represent and identify each card in the hand. Now, let’s discuss how to represent melds. For instance, consider the first three cards of the hand mentioned earlier — (5♣, 6♣, 7♣). We can create a unique hash for this meld by adding the unique representations of each card: (1 + 2 + 4) = 7. This sum represents the unique hash for the meld (5♣, 6♣, 7♣).</p><p>Now, let’s see how we store the melds using these hashes. Consider the hand (5♣, 6♣, 7♣, 8♣, 9♣, 7♠, 8♠, 9♠, 10♠, J♥, K♥, A♥, K♦):</p><ol><li><strong>Hash for the pure sequence (5♣, 6♣, 7♣, 8♣, 9♣):</strong><br>(1 + 2 + 4 + 8 + 16) = 31</li><li><strong>Hash for the pure sequence (7♠, 8♠, 9♠, 10♠):</strong><br>(32 + 64 + 128 + 256) = 480</li><li><strong>Hash for the pure sequence (J♥, K♥, A♥):</strong><br>(512 + 1024 + 2048) = 3584</li><li><strong>Hash for the last invalid meld (K♦):</strong><br>(4096) = 4096</li></ol><p>To find the total hash for the hand, we sum these values:<br>31 + 480 + 3584 + 4096 = 8191.<br>As you can see that grouping cards and calculating the hash will always sum to 8191. We can conclude that if we permute through all the combinations of meld in the hand and sum up their hash, we will get 8191 giving us a way to memorize all possible pure sequences, impure sequences and sets.</p><h3>Steps of the algorithm</h3><p>To determine the optimal grouping and points for the hand, we can follow these steps:</p><p><strong>Find All Possible Pure Sequences:</strong></p><ul><li>Generate pure sequences of lengths 3 to 5 and store their hashes.</li><li>If no pure sequences are found, calculate the total points for the hand.</li></ul><p><strong>Check for Impure Sequences:</strong></p><ul><li>If at least one pure sequence exists, find all possible impure sequences and store their hashes.</li><li>If no impure sequences are found, check for non-overlapping pure sequences calculated in Step 1. If none are found, calculate the points for the cards not included in the only pure sequence.</li></ul><p><strong>Evaluate Non-Overlapping Pure Sequences:</strong></p><ul><li>If there are at least two non-overlapping pure sequences or one pure and one impure sequence, identify all possible sets in the hand and store their hashes.</li></ul><p><strong>Dynamic Programming Approach:</strong></p><ul><li>Once all sets and sequences are calculated, utilize dynamic programming to explore all valid combinations of these sets and sequences, optimizing for the lowest total points.</li></ul><p>By systematically approaching the problem in this manner, we ensure that we cover all potential combinations and can determine the best grouping of cards effectively.</p><p>Lets Code the algorithm,</p><p>Let’s illustrate the pre-processing of a hand by converting it into mappings of suit to card and card to suit.</p><pre># hand of the user<br>hand = [&#39;1h&#39;, &#39;2h&#39;, &#39;3h&#39;, &#39;4h&#39;, &#39;1h&#39;, &#39;2h&#39;, &#39;3h&#39;, &#39;4h&#39;, &#39;4d&#39;, &#39;12h&#39;, &#39;13h&#39;, &#39;5h&#39;, &#39;j&#39;]<br># wild card<br>joker = &#39;4d&#39;<br>if joker == &#39;j&#39;:<br>    joker = &#39;1h&#39;<br># pre-processing of the hand and joker<br>hand_inverted = [card[-1]+card[:-1] if len(card)&gt;1 else &#39;j&#39; for card in hand]<br>hand_array = [[card[-1],int(card[:-1])] if len(card)&gt;1 else [-1,20] for card in hand]<br>joker_inverted = [&#39;s&#39;+joker[:-1],&#39;c&#39;+joker[:-1],&#39;d&#39;+joker[:-1],&#39;h&#39;+joker[:-1]]<br>hand_temp = []<br>all_jokers = []<br>loc_mapping = {}<br>for i,h in enumerate(hand_inverted):<br>    if h in joker_inverted or h == &#39;j&#39;:<br>        all_jokers.append((h,i))<br>    if h in loc_mapping:<br>        loc_mapping[h].append(i)<br>    else:<br>        if h != &#39;j&#39;:<br>            hand_temp.append(h)<br>        loc_mapping[h] = [i]<br>hand_inverted = hand_temp<br>different_suits,different_suits_without_joker = get_suit_card_mapping(hand_inverted,joker_inverted)<br>different_cards = get_card_suit_mapping(hand_inverted,joker_inverted)<br>good_hands = {1:set(),2:set(),3:set()}<br># calculate initial hand score<br>output = [calculcate_hs(range(len(hand)),hand_array,hand,joker),[2**i for i in range(0,len(hand))]]<br>final_output = []</pre><p><strong><em>Find All Possible Pure Sequences</em></strong>, we get pure sequences and convert it to a hash and store it into a map.</p><pre># logic to get all possible pure sequences in the hand<br>def get_pure_sequences(different_suits):<br>    all_possible_pure_seqeunces = []<br>    for length in range(3,6):<br>        for card_type,cards in different_suits.items():<br>            current_seq = []<br>            if 1 in cards:<br>                cards = cards + [14]<br>            for card in cards:<br>                if len(current_seq) == 0 or current_seq[-1]+1 == card:<br>                    current_seq.append(card)<br>                else:<br>                    current_seq = [card]<br>                if len(current_seq)==length:<br>                    all_possible_pure_seqeunces.append([f&quot;{card_type}{1 if i == 14 else i}&quot; for i in current_seq])<br>                    current_seq = current_seq[1:]<br>    return all_possible_pure_seqeunces<br># we first look for at least one pure sequences<br>seq = get_pure_sequences(different_suits)<br># we store all the pure sequences in a map<br>for i in seq:<br>    mark_location(i,loc_mapping,0,0,good_hands,1)</pre><p>Implementing <strong><em>Check for Impure Sequences and Evaluate Non-Overlapping Pure Sequences</em></strong>, we get impure sequences and if impure sequences exists we also calculate sets. We convert it to a hash and store it into a map as we did for the pure sequences above.</p><p>Helper function to check whether a sequence in impure or not and to get all possible sets.</p><pre># function to check where a sequences is an impure sequence or not.<br>def check_impure_seq(l,jokers):<br>        if len(l)&gt;1:<br>            d = shift(l,1,cval=l[0]-1)+1<br>        else:<br>            d = l<br>        s = sum(l-d)<br>        if s&lt;=len(jokers):<br>            return s<br>        elif 1 in l:<br>            l[l==1] = 14<br>            l = np.sort(l)<br>            if len(l)&gt;1:<br>                d = shift(l,1,cval=l[0]-1)+1<br>            else:<br>                d = l<br>            s = sum(l-d)<br>            if s&lt;=len(jokers):<br>                return s<br>        return -1<br># function to get all possible sets in the hand<br>def get_sets(different_suits,all_jokers,joker_combinations):<br>        all_possible_sets = []<br>        # below we generate combinations for cards with same number but different suits<br>        for card in different_suits:<br>            if len(different_suits[card]) &gt;= 3:<br>                for length in range(3,len(different_suits[card])+1):<br>                    combs = itertools.combinations(different_suits[card],length)<br>                    for comb in combs:<br>                        s = []<br>                        for val in comb:<br>                            s.append(str(val)+str(card))<br>                        all_possible_sets.append(s)<br>        # below we generate combinations for cards with same number but different suits and also including the joker<br>        for length in range(max(2,3-len(all_jokers)),4):<br>            for joker_length in range(max(1,3-length),min(4-length+1,len(all_jokers)+1)):<br>                for card,card_type in different_suits.items():<br>                    if len(card_type)&gt;=length:<br>                        combs = list(itertools.combinations(card_type,length))<br>                        for comb in combs:<br>                            current_seq = []<br>                            for val in comb:<br>                                current_seq.append(str(val)+str(card))<br>                            for item in joker_combinations[joker_length-1]:<br>                                all_possible_sets.append(current_seq+list(item))<br>        return all_possible_sets</pre><p>We iterate through all the combinations involving joker and check if it is an impure sequence or not.</p><pre># now we create optimal hands based on valid declaration.<br># if we have at least one pure sequence then only we can make a valid declaration.<br>if len(seq)&gt;0:<br>    set_flag = False<br>    # now we check if there is at least one impure sequence or non-overlapping pure sequence.<br>    joker_combinations = [list(itertools.combinations(all_jokers,i)) for i in range(1,min(6,len(all_jokers)+1))]<br>    if len(all_jokers)&gt;0:<br>        seq = []<br>        for t in range(2,len(joker_combinations)):<br>            for p in joker_combinations[t]:<br>                seq.append(list(p))<br>        for card_suit,cards in different_suits_without_joker.items():<br>            for x in range(max(1,3-len(all_jokers)),5):<br>                vals = itertools.combinations(cards,x)<br>                for val in vals:<br>                    val = list(val)<br>                    t = check_impure_seq(np.array(val),all_jokers)<br>                    tt = [f&quot;{card_suit}{itr}&quot; for itr in val]<br>                    if t != -1:<br>                        for i in range(max(1,t)+max(0,(3-x)-1),min(5-x+1,len(all_jokers)+1)):<br>                            for p in joker_combinations[i-1]:<br>                                seq.append(tt+list(p))<br>        for i in seq:<br>            mark_location(i,loc_mapping,0,0,good_hands,2)<br>        good_hands[2] = good_hands[2]-good_hands[1]<br>        # if there is an impure sequences we look for sets in the hand.<br>        if len(seq)&gt;0:<br>            set_flag = True<br>            seq = get_sets(different_cards,all_jokers,joker_combinations)<br>            for i in seq:<br>                mark_location(i,loc_mapping,0,0,good_hands,3)<br>    # if we have at least two pure sequences and sets in the hand.<br>    if len(seq)&gt;1 and not set_flag:<br>        seq = get_sets(different_cards,all_jokers,joker_combinations)<br>        for i in seq:<br>            mark_location(i,loc_mapping,0,0,good_hands,3)<br># code below is part of the if block of the pure sequence which is <br># the first if block of this section.</pre><p>Continuing in the same if section as above, we perform memorization of each meld and store the best possible valid combination with each grouped meld.</p><pre># in the same if block as above.<br>    ans = [200]*2**len(hand)<br>    optimal_hand = [None]*2**len(hand)<br>    # below we look for a set of non-overlapping pure/impure/sets that gives the minimum score.<br>    def get_permutation(current_hand,optimal_hand,ans,hand_array,hand,joker,good_hands):<br>        if current_hand == 0:<br>            return 0, []<br>        elif ans[current_hand] != 200:<br>            return ans[current_hand],optimal_hand[current_hand]<br>        else:<br>            flag = False<br>            for tc in [1,2,3]:<br>                for pos in good_hands[tc]:<br>                    if pos&amp;current_hand == pos:<br>                        flag = True<br>                        nscore,o = get_permutation(current_hand^pos,optimal_hand,ans,hand_array,hand,joker,good_hands)                        <br>                        if nscore&lt;ans[current_hand]:<br>                            ans[current_hand] = nscore<br>                            optimal_hand[current_hand] = [pos] + o<br>            if not flag:<br>                pos = get_set_pos(current_hand)<br>                nscore = calculcate_hs(pos,hand_array,hand,joker)<br>                ans[current_hand] = nscore<br>                optimal_hand[current_hand] = [current_hand]<br>            return ans[current_hand],optimal_hand[current_hand]<br>    # below we map the hash to the card and calculate the hand score.<br>    hand_max = 2**len(hand)-1<br>    for j in good_hands[3]:<br>        ans[j] = 0<br>        optimal_hand[j] = [j]<br>    for i in good_hands[1]:<br>        ans[i] = 0<br>        optimal_hand[i] = [i]<br>        left_hand = hand_max^i<br>        score = calculcate_hs(get_set_pos(left_hand),hand_array,hand,joker)<br>        if score&lt;output[0]:<br>            output[0] = score<br>            output[1] = [i] + [left_hand]<br>    for j in good_hands[2]:<br>        ans[j] = 0<br>        optimal_hand[j] = [j]<br>    flag = False<br>    for i in good_hands[1]:<br>        for j in good_hands[2]:<br>            if i&amp;j==0:<br>                ans[i|j] = 0<br>                optimal_hand[i|j] = [i,j]<br>                score,oh = get_permutation(hand_max^(i|j),optimal_hand,ans,hand_array,hand,joker,good_hands)<br>                if score&lt;output[0]:<br>                    output[0] = score<br>                    output[1] = [i,j] + oh<br>                if score == 0:<br>                    flag = True<br>        if flag:<br>            break<br>        for j in good_hands[1]:<br>            if i&amp;j==0:<br>                ans[i|j] = 0<br>                optimal_hand[i|j] = [i,j]<br>                score,oh = get_permutation(hand_max^(i|j),optimal_hand,ans,hand_array,hand,joker,good_hands)<br>                if score&lt;output[0]:<br>                    output[0] = score<br>                    output[1] = [i,j] + oh<br>                if score == 0:<br>                    flag = True<br>        if flag:<br>            break<br>    for output_hand in output[1]:<br>        h = []<br>        for location in get_set_pos(output_hand):<br>            h.append(hand[location]) <br>        final_output.append(h)<br>print(&#39;Grouped Hand = &#39; + str(final_output))<br>print(&#39;Hand Points = &#39; + str(min(80,output[0])))</pre><p>The above code has a computational complexity of ²¹³ + ²¹³ + ²¹³= 24576 with average case complexity of Θ(²¹³) and runs under 10 milliseconds.</p><h3>Conclusion</h3><p>We’ll explore the most popular online RMG of Rummy, sharing tips and tricks to maximize wins and minimize losses. We’ll also address the challenge of optimal grouping using dynamic programming with bit-masking, along with the relevant code.</p><h3>Author</h3><p><a href="https://in.linkedin.com/in/naman-lal-246077113">Naman Lal </a>— He specialises in leveraging machine learning and artificial intelligence to transform data into actionable insights. With a focus on automating decision-making and solving complex problems, he is dedicated to driving innovation and efficiency in the tech landscape.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0ecd44793251" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>