<?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[OwnID Engineering - Medium]]></title>
        <description><![CDATA[Our engineering blog is a platform for sharing insights and knowledge on cutting-edge technologies, engineering practices, and industry trends. Join us on a journey of discovery, innovation, and problem-solving. - Medium]]></description>
        <link>https://medium.com/ownid-engineering?source=rss----5dc036f5f73e---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>OwnID Engineering - Medium</title>
            <link>https://medium.com/ownid-engineering?source=rss----5dc036f5f73e---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 17:51:21 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/ownid-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[[Part 4] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey]]></title>
            <link>https://medium.com/ownid-engineering/part-4-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-988d2ad65b5e?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/988d2ad65b5e</guid>
            <category><![CDATA[performance-testing]]></category>
            <category><![CDATA[load-testing]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[cloud-cost-optimization]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Daniel Idlis]]></dc:creator>
            <pubDate>Wed, 20 Dec 2023 17:25:21 GMT</pubDate>
            <atom:updated>2024-05-25T09:23:37.558Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Znt5irlCWNgdm4MeOyP9kA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@priscilladupreez?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Priscilla Du Preez 🇨🇦</a> on <a href="https://unsplash.com/photos/three-men-laughing-while-looking-in-the-laptop-inside-room-XkKCui44iM0?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p><a href="https://medium.com/ownid-engineering/part-3-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-ee4cbe9aefe7">The previous article</a> described the steps that we took in order to analyze and optimize our system’s performance, primarily focusing on OwnID’s caching mechanism. However, we still wanted to evaluate how the Kubernetes computing resources were allocated across our different backend services. Additionally, we were not satisfied with the results of our tests as every test run had too many errors even though we knew they originated from a third-party component. In this part I will delve into how we optimized the backend service’s resource utilization and removed the background noise from our tests to better focus them on OwnID’s internal components. Finally, I will summarize the key takeaways and lessons learned from the project.</p><h3>Kubernetes resources optimization</h3><p>Similar to previous iterations, we began by taking a closer look at the current situation. Examining the metrics from our previous test runs, we identified that one of the services’ pods briefly exceeded its limit of <strong>0.8</strong> <strong>CPU</strong>, reaching around <strong>1.25</strong>. This meant that the two pods that were deployed couldn’t handle our expected load without scaling out (i.e. triggering a deployment of more pods, temporarily hurting the performance of the system). As the difference between the required and allocated CPU was small, we decided that increasing the CPU limit could eliminate additional pod deployments, leading to a more consistent performance. However, this approach has its limitations. For services requiring significantly more resources to execute a task, horizontal scaling remains the superior option, assuming the workload can be distributed across multiple instances effectively.</p><p>For the next test run we modified all of our services that had excessive or insufficient resources based on the metrics that we observed during previous test runs. The objective was to evaluate whether the performance could be maintained with these modifications.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yV0x9_nBEKx-MiBp" /></figure><p>As the test summary suggested, the performance was preserved (compared to previous test runs) so we decided to adopt this configuration as our final setup.</p><h3>Neglecting third-party components</h3><p>After fine tuning our computing resources, we wanted to focus the tests on OwnID’s internal components. This was important because we wanted to get a clear picture of our system’s performance, without the side effects of external components. Previous test iterations relied on an actual CIAM system, which we found to be problematic as we didn’t have control over that component’s scaling capabilities. By removing the CIAM variable from the equation, we were able to better identify the point in which our system stops delivering a sufficient performance. To do so, the test environment was set up with one important change — a custom implementation of the <a href="https://docs.ownid.com/quickstart/get-started">full-stack integration</a> was used as the CIAM system in the test. This special kind of integration allows our customers to have full control over the access to their CIAM system by exposing 3 simple HTTP endpoints that implement OwnID’s integration interface. Instead of accessing the CIAM system directly, OwnID calls the client’s HTTP endpoints.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X4QI9cbz5YRQJXBy" /><figcaption>An overview of OwnID’s full-stack integration</figcaption></figure><p>For this test we used our own implementation of the 3 HTTP endpoints. This implementation always returned a valid predefined response so we effectively mocked this component’s behavior. This enabled us to focus on OwnID’s part in the authentication process, removing any background noise caused by third-party components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/490/0*vra-IweYzRdSse2G" /><figcaption>An overview of the current test architecture</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HfocfWdbFquGg5yc" /></figure><p>As you can see, the percentage of failed requests was extremely low (<strong>0.01%</strong>). For about <strong>70%</strong> of the test (up to roughly <strong>1800</strong> <strong>requests per second</strong>) the P95 response time was <strong>&lt;100</strong> <strong>milliseconds</strong>. A quick look into the DataDog logs showed that all of the errors happened in the integration server which is the component that communicates with the customer’s CIAM system. The root cause was that the mocked full-stack integration backend which was developed for the purpose of the test did not have an auto scaling mechanism defined. It stopped responding once it received too many requests as it couldn’t scale any further. The Kubernetes manifest of this component defined a single pod with <strong>0.2</strong> <strong>CPU</strong> and <strong>1</strong> <strong>GB </strong>of memory. Looking at the pod’s resource usage throughout the test, we could easily see that the CPU was constantly spiking above the defined limit, starting a few minutes before the spike in response time:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qHwQib2y-N59p_01" /></figure><p>From the results we’ve seen, we concluded that the bottleneck was indeed the mocked full-stack integration backend. This assured us that OwnID could theoretically handle at least <strong>1800</strong> <strong>RPS</strong>. To get a more accurate number, we knew that we needed to enable auto scaling for the mocked full-stack integration backend and run the test for a longer period of time with the same RPS configuration.</p><h3>Last but not least</h3><p>After the last test run we were still not completely convinced that we have achieved our desired results. We added an autoscaling policy for the mocked full-stack integration backend and this time we capped the RPS at <strong>2500</strong> as it was already several orders of magnitude higher than our required target.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Kcy8A1gnnuofxdzS" /></figure><p>As you can see in the results, we finally had a test that passed our criteria. The error rate was <strong>0.04%</strong> (threshold was defined at <strong>0.1%</strong>) and the P95 response time was <strong>65 milliseconds</strong> (threshold was defined at <strong>100 milliseconds</strong>). After reviewing the vital signs of the entire system’s components, we could finally conclude that we had achieved our goal.</p><p>This meant I could finally give my manager the good news: we can confidently onboard the new client that was the reason for this project</p><h3>Summary</h3><p>This project was an intense yet rewarding journey. Pushing our system to its limits revealed invaluable insights about its strengths and weaknesses, allowing us to take action in order to meet the business needs. Along with the increased performance, we were able to achieve a remarkable <strong>30%</strong> reduction in overall cloud costs.</p><p>These are the key takeaways and lessons learned:</p><ul><li><strong>Goals definition </strong>— Set simple, measurable goals for the tests to clearly distinguish a success from a failure.</li><li><strong>Offline analysis - </strong>Inspect your system’s architecture proactively and try to find potential bottlenecks. It will be much easier to identify problems down the road when you know where to look for them.</li><li><strong>Scope definition - </strong>Clearly define which components are out of the testing scope and make sure you neglect their effect on the test results. This is especially important with third-party services that you might create an unexpected load on.</li><li><strong>Baseline setup - </strong>Make sure you have a solid, reliable baseline for the test results. It will help you evaluate progress more effectively.</li><li><strong>Automation - </strong>Create a way for you to run tests in a single click so the feedback loop is faster and more efficient.</li><li><strong>Timeline planning - </strong>Expect to have more test runs than you initially plan for as testing often reveals unforeseen issues, requiring additional test runs to explore and address them.</li><li><strong>Cost awareness - </strong>Consider the additional costs associated with the testing process. On top of the price of K6 (our chosen testing platform), our AWS &amp; DataDog bills were <strong>1.5 </strong>times higher than usual in the testing period.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=988d2ad65b5e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/part-4-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-988d2ad65b5e">[Part 4] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[Part 3] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey]]></title>
            <link>https://medium.com/ownid-engineering/part-3-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-ee4cbe9aefe7?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/ee4cbe9aefe7</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[performance-testing]]></category>
            <category><![CDATA[cloud-cost-optimization]]></category>
            <category><![CDATA[load-testing]]></category>
            <dc:creator><![CDATA[Daniel Idlis]]></dc:creator>
            <pubDate>Wed, 13 Dec 2023 16:40:27 GMT</pubDate>
            <atom:updated>2023-12-20T17:26:28.477Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jwsGPNhgEMmjIaKH4u2HcA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@johnschno?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">John Schnobrich</a> on <a href="https://unsplash.com/photos/person-using-laptop-FlPc9_VocJ4?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p><a href="https://medium.com/ownid-engineering/part-2-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-4848270fc46b">The previous article in the series</a> introduced OwnID’s architecture, described its potential performance bottlenecks and outlined the process of test planning and implementation while emphasizing the role of system observability in monitoring critical parameters during test execution. This article will highlight key test iterations, the issues that arose and how they were addressed.</p><h3>Initial smoke test</h3><p>To verify the functionality of the test setup and create a baseline for comparing future tests, we started with running a gradually increasing load on the system, mimicking the anticipated real-world traffic. This test was conducted with a system configuration that’s identical to our production environment in terms of computing resources, with the simulated load ramping up from <strong>0</strong> RPS to our target RPS of <strong>40</strong>. Shortly after initiating the test, we observed an unexpectedly high error rate from Redis which indicated that it already has performance issues:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/951/0*by6_PfdwvQYD-d2T" /><figcaption>DataDog logs from our Redis cluster</figcaption></figure><p>A closer examination of the errors showed that there were too many concurrent read attempts, resulting in request timeouts. After proving that our initial assumption was correct and the Redis cache is indeed a performance bottleneck, we knew its configuration had to change.</p><h3>Scaling up Redis</h3><p>As Redis was already having issues in its current configuration, our first step was to attempt to scale up Redis by adding more resources to the existing cluster. This was much easier than scaling out because we were using a Redis cluster that was hosted on AWS ElastiCache with cluster mode <strong>disabled</strong>. Up until Redis 7, scaling out such clusters <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/VersionManagement.html">required a cluster migration</a>. Our production instance was using a <strong>cache.m5.large</strong> machine that has <strong>2 vCPUs</strong> and <strong>6.4 GB RAM</strong> so we decided to try out a much stronger <strong>cache.m4.4xlarge</strong> machine that has <strong>16 vCPU</strong> and <strong>60 GB RAM</strong>. We intentionally used an extremely strong machine so we could have a clear answer to whether making the machine stronger will solve the problem we found. The results were clear pretty fast — it did help, but certainly not enough:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4RaXyr5ci85CtZt5" /></figure><p>As seen in the test summary, the quick and easy solution of scaling up our Redis cluster didn’t help much. It meant that we have to go through the cluster migration process so we can enable cluster mode and utilize Redis’ sharding mechanism.</p><h3>Scaling out Redis</h3><p>The second thing we wanted to try was scaling out, which meant splitting our Redis cluster into multiple shards. As mentioned above — we were using AWS ElastiCache with cluster mode <strong>disabled.</strong> To be able to scale out we had to create a new cluster with cluster mode <strong>enabled</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*EGn1_oJ1GkvKumYe" /><figcaption>Redis cluster modes</figcaption></figure><p>The machine type was changed back to the same as production (<strong>cache.m5.large</strong>) but we configured <strong>10</strong> shards instead of <strong>1</strong>. Every shard now contained <strong>1</strong> primary (write) node and <strong>3</strong> read replicas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*k9lj4PxuK7Kr4luy" /></figure><p>As you can see — the error rate and response time were significantly lower, though clearly not low enough. DataDog still showed timeout errors from Redis but the frequency was significantly lower due to our change in the configuration of the cluster.</p><p>The <a href="https://www.datadoghq.com/dashboards/elasticache-dashboard-redis/">Datadog ElastiCache dashboard</a> showed us something very interesting — one of the shards was getting 10 times more get requests than the other instances:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/582/0*8xOkA2PmK0rJKAGl" /></figure><p>The uneven distribution of requests across cache shards suggested the presence of a <strong>hot spot</strong> — a particular entry that was being accessed much more frequently than the others. This was confirmed by investigating the logs, which revealed that the OwnID app configuration was being retrieved from the cache upon every request made to the backend server. Eliminating these get operations from Redis was not possible since the backend server, being stateless, needed this data freshly fetched for every request. To address this issue, we decided to introduce an additional caching layer within the backend server.</p><h3>Implementing an in-memory cache</h3><p>For this test run, an in-memory cache for app configurations was implemented within the backend server. This cache will serve as the primary source for retrieving app configurations, with Redis acting as a fallback mechanism in case the relevant data is missing from the in-memory cache.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MExNZFyX9KdwmS3g" /></figure><p>As you can see — the P95 response time got <strong>99%</strong> smaller — from <strong>12.7 seconds</strong> to around <strong>100 milliseconds. </strong>The error rate was still greater than zero but was reduced from <strong>3.6%</strong> to <strong>1.4%</strong>.<strong> </strong>What stood out in this test run was the unusual number of errors in the integration server:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/956/0*JV5rwOFz-f5v_Z0T" /></figure><p>As previously mentioned, the integrations server is a component that is in charge of communicating with the CIAM system of our clients. DataDog showed us that the errors were happening on a specific API call to Gigya (the test application’s CIAM system):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EVyfh4NXmvZOVl4k" /></figure><p>This is the API call that OwnID performs to get a user’s session which will be later forwarded to the client-side to log the user in. This request was failing due to timeouts from Gigya, caused by the fact that OwnID app that was tested was using a Gigya account with a basic subscription tier. This tier has a really low request limit which we easily passed during the test.</p><p>This confirmed that our initial concern regarding Redis had been resolved, and the overall performance seemed stable. Our attention now shifted to optimizing the resource utilization of the new Redis cluster. The challenge was to determine the minimum resource allocation that would sustain the current performance while providing capacity for future growth. We decided to temporarily put aside the high number of errors as they were all coming from a 3rd party component but rest assured, they will be addressed in the next article.</p><h3>Optimizing the Redis configuration</h3><p>To achieve the optimal Redis configuration, we had to analyze the ratio between read and write operations to determine the amount of read replicas that should be used per shard. This required a comprehensive examination of the current Redis cluster in production.</p><p>DataDog’s ElastiCache dashboard unveiled that the ratio between get and set operations is very high as expected:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/530/0*ijEHUisP38NC_Ohn" /></figure><p>As we wanted to tune the configuration to match this proportion as much as possible for the best performance, we had to use the largest amount of read replicas that ElastiCache could provide which is <strong>5</strong> per shard. We also wanted to find the optimal node type for each node within the cluster. Looking at the memory usage of each node during the last test, we saw that only about <strong>8.5 MiBs</strong> were being used by each node:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/0*FIj1qYsWQxvxAkri" /></figure><p>This means that we can use a significantly weaker node type as long as we are going to increase the number of nodes that can handle get operations. For the next test run, the Redis cluster was scaled down to have only <strong>2</strong> shards with <strong>1</strong> primary node and <strong>5</strong> read replicas each. The node type was changed to <strong>cache.t2.micro</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*eT9Euwgi7wCA7OnN" /></figure><p>The test results demonstrated that this Redis configuration did not only maintain the acceptable performance levels, but also resulted in a significant cost reduction of approximately <strong>35%</strong> in our monthly AWS bill for ElastiCache (according to AWS’ <a href="https://calculator.aws/#/createCalculator/ElastiCache">ElastiCache pricing calculator</a>). This substantial cost saving, coupled with the sustained performance, strongly supported the adoption of the proposed Redis configuration.</p><p>The next steps in our performance optimization journey involved some fine tuning of our backend services’ computing resources and also tackling the errors that we still kept encountering during test runs. <a href="https://medium.com/ownid-engineering/part-4-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-988d2ad65b5e">In the next and final article of this series</a>, we’ll delve into the process of resource optimization and also eliminating third-party components, enabling us to gain a clear understanding of our system’s true performance. We’ll also conclude the project and present key takeaways and lessons learned.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ee4cbe9aefe7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/part-3-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-ee4cbe9aefe7">[Part 3] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[Part 2] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey]]></title>
            <link>https://medium.com/ownid-engineering/part-2-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-4848270fc46b?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/4848270fc46b</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[cloud-cost-optimization]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[load-testing]]></category>
            <category><![CDATA[performance-testing]]></category>
            <dc:creator><![CDATA[Daniel Idlis]]></dc:creator>
            <pubDate>Wed, 06 Dec 2023 15:40:07 GMT</pubDate>
            <atom:updated>2024-05-25T08:47:19.216Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Q2HYFNRY0b4OYT3UqHdLg.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@campaign_creators?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Campaign Creators</a> on <a href="https://unsplash.com/photos/person-using-macbook-pro-pypeCEaJeZY?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p><a href="https://medium.com/p/04e91e41cfae">In the first article of the series</a> I outlined the project’s motivation, problem definition, and tool selection process. This article will focus on our test planning and implementation, emphasizing the significance of having good observability of the system and its components. But first, let’s review our high-level architecture and discuss how we identified potential architectural weaknesses and planned to test and address them.</p><h3>The architecture in a nutshell</h3><p>Our architecture is based on standard client-server communication through HTTP. The main components of the system are:</p><ul><li><strong>WebSDK:</strong> A frontend library that our clients use to add the OwnID widget to their websites. It interacts with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API">WebAuthn API</a>, which enables us to trigger the biometric authentication on the user’s device.</li><li><strong>Console Server:</strong> A service responsible for storing the internal configurations of OwnID applications, which are managed through <a href="https://console.ownid.com/">the OwnID console</a>. These configurations control various aspects of the product functionality and user experience. For instance, they may define whether newly created user accounts must go through email verification or control the visual appearance of the OwnID widget (its color, positioning etc.)</li><li><strong>Backend Server:</strong> A service that orchestrates the entire authentication process from start to finish according to user interaction and internal configurations. Its state machine serves as the brain of the WebSDK, determining the possible actions that the user can take in each step of the authentication process.</li><li><strong>Redis Cache:</strong> A standard cache that stores internal configurations to reduce the load on the console server’s database. It also stores the state of every authentication process that goes through OwnID.</li><li><strong>Integrations Server:</strong> A service responsible for communicating with clients’ identity management systems (CIAMs). It can perform CRUD operations on users and also generate a session after a successful OwnID authentication.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/674/0*OYzxierYGh35UObi" /><figcaption>A simplified overview of OwnID’s architecture</figcaption></figure><p>As previously mentioned, Redis stores the state of each authentication process and the internal configurations from the console server. Both of these resources are necessary at each step of the authentication journey and as a result they are accessed very frequently. Therefore, it is essential to examine the Redis cluster’s performance under heavy load.</p><p>Moreover, as the traffic through our system increases, our services’ autoscaling policies in Kubernetes will start deploying more pods for each service. At a certain point, the Kubernetes cluster’s nodes will also need to scale in order to accommodate the growing number of pods. Both of these policies haven’t been properly tested before and require a thorough evaluation.</p><h3>Suspect #1</h3><p>Redis was our primary suspect for being a performance bottleneck. The main reason for this was the significant amount of reads and writes to Redis during each authentication flow — a user’s most common interaction with OwnID. As mentioned before, OwnID’s backend server uses a state object to keep track of users’ progress during an authentication flow. It also uses the internal configurations of the OwnID application in order to make decisions that determine the user journey. Our backend server needs to fetch and update these two resources upon every request from our WebSDK. The state object is relatively small in size so we didn’t worry much about its effect on the system performance, but the internal configurations can be relatively large in size.</p><p>This seemed like a potential issue at a large scale as we were running a single Redis shard with 1 read replica hosted on AWS ElastiCache with <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Replication.Redis-RedisCluster.html">cluster mode disabled</a>. In this configuration, Redis maintains a single shard, inside of which is a collection of Redis nodes: one primary read/write node and up to five secondary, read-only replica nodes. This setup allowed us to scale out read operations to a limited extent (up to five read replicas), but writes could not be scaled horizontally at all.</p><p>These factors raised our concerns about Redis’ ability to handle the increased load efficiently in its current configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*P_irSje1hga4TW3q" /><figcaption>Redis cluster modes</figcaption></figure><h3>Suspect #2</h3><p>Another part of our system that we wanted to inspect under heavy load was our Kubernetes cluster.</p><p>Our backend services are running inside an AWS EKS cluster and each one has an Horizontal Pod Autoscaler (<a href="https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/">HPA</a>) responsible for pods’ horizontal scaling if CPU or memory thresholds are exceeded. The resource allocation for each service’s pods along with the thresholds defined in the HPA manifest, determine when Kubernetes should deploy additional pods for a given service. Both the resource allocation and HPA thresholds were defined when the very first version of the OwnID was launched. These configurations have remained unchanged since then, posing a potential bottleneck that we wanted to investigate.</p><p>Along with the configurations mentioned above, we also wanted to evaluate our EKS node autoscaling policy. This policy is responsible for provisioning EC2 instances to the Kubernetes cluster when the existing nodes can no longer accommodate new pods that are awaiting deployment. With the significant increase in traffic, we wanted to see the node allocation’s impact on the system’s performance and the end-user experience.</p><h3>Test planning</h3><p>Our goal was to check the components mentioned above by having three test scenarios that will run in parallel and simulate the actual traffic our servers were expected to receive:</p><ul><li>State object creation (widget load on an end-user’s browser)</li><li>Login</li><li>Registration</li></ul><p>Each scenario should try to mimic the exact flow of HTTP requests between the client and server to ensure that we are simulating a realistic real-world use-case as much as possible. It was also really important to remove irrelevant background noise from the test. In our case it is the <strong>client’s CIAM system</strong> (which is an external component in terms of our architecture) and the <strong>WebAuthn API</strong> that will be treated as a black box so their performance won’t affect the test results. This meant that we would have to mock their behavior to neglect their effect on the system performance as much as possible.</p><p>In the first test iterations the CIAM system was not mocked as we understood it would take too much time and effort to do so. We were okay with this component’s effect on the performance as long as we saw the other components were behaving as expected. We later conducted a test that neglected the CIAM system which I will describe later in the article series.</p><p>The WebAuthn API that is called by our WebSDK was mocked by an AWS Lambda function that provided us the basic WebAuthn functionality of <a href="https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create#web_authentication_api">creating</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#web_authentication_api">using</a> credentials. It used a hardcoded pair of public &amp; private keys to ensure that we always create and use the same credential as our tests always use the same user for registration and login.</p><p>The architecture of all the involved components in the test at this point looked like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/492/0*JY0nPW7P6RqssYIQ" /><figcaption>Overview of the test’s components</figcaption></figure><h3>Test preparation</h3><p>To accurately test the performance of every component, the testing environment must be identical to the production environment. Ideally, a replica of the production environment should be created but at the time of working on this project, we did not have the capability to do that easily. What we found to be a great mid-way solution is to use our staging environment (that is only used for QA purposes) and tweak the relevant parameters to make it match the production environment in terms of computing resources.</p><p>This included:</p><ul><li>Backend services’ deployment resources in Kubernetes</li><li>AWS EKS node machine type</li><li>AWS ElastiCache machine type</li><li>Client configuration for the OwnID application that was used for testing</li></ul><p>It’s also important to mention that our console server’s databse (the one that stores the internal configurations) was left out of scope as we rely on Redis having the data we need 99.9% of the time. The fallback to the database happens very rarely as all of our large clients have their client configurations permanently stored inside Redis (without a TTL) to improve performance. This means that we can neglect the database which barely receives any requests in our standard everyday use case.</p><h3>Test implementation</h3><p>Following our initial proof-of-concept, we were already familiar with modeling HTTP requests with K6’s <a href="https://k6.io/docs/test-authoring/create-tests-from-recordings/using-the-browser-recorder/">browser recorder</a>.</p><p>The next challenge was to orchestrate these requests into a cohesive authentication flow which is essentially the sequence of requests that the OwnID WebSDK sends to the backend server.</p><p>Shortly after starting to write the first test scenario, it became apparent that the codebase would expand significantly, requiring us to properly manage it in a GitHub repository.</p><p>With a modular approach in mind we separated each HTTP request to its own dedicated function:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e686488f3872834f73263ce437be3fa0/href">https://medium.com/media/e686488f3872834f73263ce437be3fa0/href</a></iframe><p>Which was later used to compose the authentication flow:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6e9e28dafd650f0d67f7bb08a062983d/href">https://medium.com/media/6e9e28dafd650f0d67f7bb08a062983d/href</a></iframe><p>Which was embedded in a test that we could run using the K6 CLI:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/228735525e2b6cabf16f4adc97ef8b35/href">https://medium.com/media/228735525e2b6cabf16f4adc97ef8b35/href</a></iframe><p>This modular approach expedited test development by ensuring the independence and reusability of each of the test’s components.</p><h3>Monitoring results</h3><p>Observability is another crucial aspect of the testing process. Reliable observation of key metrics and parameters during test execution is essential for obtaining accurate test results at the end of the process. Our goal was to ensure that we had a way to monitor the following parameters:</p><p><strong>AWS ElastiCache</strong></p><ul><li>CPU utilization</li><li>Memory usage</li><li>Get / set requests ratio</li></ul><p><strong>AWS EKS</strong></p><ul><li>Number of pods (per service)</li><li>Pod CPU utilization</li><li>Pod memory usage</li><li>Number of nodes</li><li>Node CPU utilization</li><li>Node memory usage</li></ul><p><strong>OwnID Services</strong></p><ul><li>Number of errors</li><li>Response time</li></ul><p>Fortunately, we already had DataDog up and running in all of our environments. Its out-of-the-box dashboards provided all the necessary metrics for analyzing every situation. K6 also provided some valuable general statistics, such as number of requests failures and P95 response time.</p><p>With our goals defined and the testing environment prepared, it was time to begin running the tests. <a href="https://medium.com/ownid-engineering/part-3-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-ee4cbe9aefe7">In the next article</a>, I will delve into the test iterations, our findings, and how we addressed the issues that arose.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4848270fc46b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/part-2-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-4848270fc46b">[Part 2] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[Part 1] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey]]></title>
            <link>https://medium.com/ownid-engineering/part-1-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-04e91e41cfae?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/04e91e41cfae</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[load-testing]]></category>
            <category><![CDATA[cloud-cost-optimization]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[performance-testing]]></category>
            <dc:creator><![CDATA[Daniel Idlis]]></dc:creator>
            <pubDate>Wed, 29 Nov 2023 16:25:39 GMT</pubDate>
            <atom:updated>2023-12-06T15:40:49.965Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i-tykrY-FWy0wPE2e7ZT6Q.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@homajob?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Scott Graham</a> on <a href="https://unsplash.com/photos/person-holding-pencil-near-laptop-computer-5fNmWej4tAA?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p>Imagine this:</p><p>You are a small startup company with a live product and a couple of very satisfied clients. Your product has a certain amount of traffic that we’ll refer to as <strong>x</strong> and your infrastructure is nicely optimized for the relatively small and convenient <strong>x</strong>.</p><p>You take a sip from your cold beer while writing an email to a colleague when suddenly the CEO bursts into the office running and screaming about a contract that was just signed: “We got them!!”</p><p>Everyone starts celebrating while your manager approaches you and sneaks in a small comment: “During the call they said that they have a relatively large number of monthly active users. Let’s just make sure we can support that before the go-live, ok?”. You immediately ask: “How much is relatively high?” and he hesitantly replies: “about <strong>8x</strong>”.</p><p>This is exactly what happened to me about a year ago. After processing the initial shock, I scheduled a meeting with my manager to properly discuss this in further detail which led to this infrastructure performance optimization journey that was one of the most interesting projects I ever got to work on.</p><p>In this article series I will walk you through the journey that I led at <a href="https://ownid.com/">OwnID</a> so you can learn how we were able to make our product more robust and resilient by utilizing the Redis sharding mechanism, optimizing k8s resources usage and some code refactorings. We also made our infrastructure more fine-tuned to the performance requirements while reducing AWS costs by thousands of dollars a month. All of this was done using some common best practices and great tools such as K6, DataDog and more.</p><p>In the first article of the series, I’m going to briefly explain what OwnID is and what was our motivation to start this performance optimization journey. I will go over the benchmark definition process and talk about what are the main considerations that you should take into account when choosing a load testing platform.</p><h3>Who are we?</h3><p><a href="https://ownid.com/">OwnID</a> is a passwordless solution for websites and mobile apps that provides the best user experience possible while leveraging the latest and most secure way of authentication — Passkeys. Our clients see a <strong>30% </strong>increase in user conversion during login &amp; registration flows on average. We provide a layer that integrates directly with the clients’ customer identity and access management system (referred to as <strong>CIAM</strong> from here on) and makes the use of passwords completely redundant. Our product is being used by some of the biggest brands in the world: Nestle, Carrefour, Carnival Cruise Lines and many more.</p><h3>Introduction</h3><p>As a young startup company you have to move fast. Moving fast in software development means you’ll always have to compromise to a certain degree on either the code quality, system performance or reliability.</p><p>After some time you get to a point in which one of the factors mentioned above becomes a problem that you have to solve as it prevents you from moving forward with delivering features and meeting business requirements.</p><p>That’s exactly how we came up to the conclusion that we need to review our performance to make sure that we can handle the expected increase in traffic through our system.</p><h3>The problem</h3><p>As we were very focused on acquiring more and more clients while only developing mandatory features, we didn’t focus too much on performance as it was good enough. This was mainly because our traffic (the number of active users) was easily manageable with the allocated resources that our system had. At the time before the project began, the OwnID servers received an average of <strong>5</strong> requests a second, roughly. We understood that we might need to review our performance and scaling capabilities as we were about to sign a deal with one the largest sports brands in the US that was expected to increase our average number of requests per second to around <strong>40</strong> which is a <strong>x8</strong> increase. I will deep dive into our product and architecture later in the series so you can better understand the user interaction for which we tried to optimize the performance.</p><h3>The business benchmark</h3><p>The first thing we had to understand is what’s our goal with this project, what exactly we are aiming for.</p><p>As explained previously, we had to be able to support up to <strong>40</strong> requests per second (referred to as <strong>RPS</strong> from here on). These requests correspond to various steps throughout a user’s authentication journey (whether it’s registration or login). The state of this journey is maintained by our backend using a mechanism that will be explained later in the article series. In terms of concrete metrics, we wanted to see if we were able to support this amount of requests with the following main thresholds:</p><ul><li><strong>Error rate (% of failed requests)</strong> should be lower than <strong>0.1%</strong> (our usual error rate in production was practically zero)</li><li><strong>Average P95 latency </strong>should be under <strong>100 milliseconds</strong> (our usual P95 latency in production was approx. 80 milliseconds)</li></ul><p>As you can see, our thresholds were set with our actual production performance in mind.</p><p>This is <strong>very</strong> important as it helps you set realistic goals and also measure them in comparison to actual, real world performance.</p><p>To be able to evaluate our performance in such a way, we decided to start a load testing project. Our next step was to find a tool that will allow us to perform such an evaluation as fast and easy as possible because as mentioned previously — the countdown towards the deadline had already started.</p><h3>Tool selection</h3><p>After defining our goals for the project, we started looking for a tool that will help us evaluate the performance of our system. Our main requirements from this tool were:</p><ul><li>Writing tests should be straightforward as we only want to send HTTP requests and pass parameters between them.</li><li>We want to be able to configure dynamic scenarios (number of executions over time, variable load etc.)</li><li>A SaaS product (infrastructure that’s managed by the product itself) is a big bonus.</li></ul><p>After reading about some of the options online and trying to create some very basic POCs, we summarized the pros and cons in the following table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/824/1*Rud07uqT_AVi28f3vZ03RQ.png" /></figure><p>As you can see, K6 was the clear winner for us as it ticked all 3 of our main requirements and also left a <strong>very</strong> good impression on us during the POC phase. Our POC was essentially a very simple attempt at modeling a few of the basic HTTP requests between our client and server.</p><p>K6 provides amazing out of the box support for <a href="https://k6.io/docs/using-k6/http-requests/">sending HTTP requests</a> so all you have to do is model the relevant HTTP requests using JavaScript. K6’s web recorder chrome extension was extremely helpful and got us up and running with a basic test in a matter of minutes as it captured and modeled all the HTTP calls between our WebSDK and backend server by itself. All you have to do is to start the recorder and go through the user journey that you want to capture. The end result will be a test written in JS that just requires you to press play.</p><p>After selecting the right tool for the job, our next step was to plan the tests and implement them. All of that is waiting for you in <a href="https://medium.com/ownid-engineering/part-2-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-4848270fc46b">part 2 of the series</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=04e91e41cfae" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/part-1-preparing-for-success-a-startups-infrastructure-performance-optimization-journey-04e91e41cfae">[Part 1] Preparing for Success: A Startup’s Infrastructure Performance Optimization Journey</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automate to Elevate: Debugging Your Local JS Code Seamlessly on Customer’s Production]]></title>
            <link>https://medium.com/ownid-engineering/automate-to-elevate-debugging-your-local-js-code-seamlessly-on-customers-production-dd26972c06cb?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/dd26972c06cb</guid>
            <category><![CDATA[api]]></category>
            <category><![CDATA[debugging]]></category>
            <category><![CDATA[chrome-devtools]]></category>
            <category><![CDATA[production]]></category>
            <category><![CDATA[rollupjs]]></category>
            <dc:creator><![CDATA[Yedidya Schwartz]]></dc:creator>
            <pubDate>Sun, 26 Nov 2023 12:57:28 GMT</pubDate>
            <atom:updated>2023-11-26T12:57:28.484Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2AUuhxOfuD-gaQRUKBfcMg.png" /><figcaption>Photo by<a href="https://unsplash.com/@nublson?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"> Nubelson Fernandes</a> on<a href="https://unsplash.com/photos/man-in-black-long-sleeve-shirt-wearing-black-headphones-sitting-on-chair-gTs2w7bu3Qo?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"> Unsplash</a></figcaption></figure><p>In this blog post, I’ll share how I leveraged <a href="https://www.linkedin.com/posts/addyosmani_programming-webdevelopers-activity-7097097872529129476-K6jW/?utm_source=share&amp;utm_medium=member_android">the new Chrome API response override feature </a>and <a href="https://rollupjs.org/plugin-development/">rollup.js custom plugins</a> to create a tool enabling the loading of a local version of our SDK for debugging on customer production websites. This automated process allows you to write code on your local machine, with changes reflected immediately on the relevant customer production website, requiring no extra actions.</p><p>This enhancement elevated our developer experience to a whole new level.</p><p>The instructions here are tailored to our use case, but the general steps are applicable to every frontend component of any application.</p><h3><strong>Our Product and Its Loading Process</strong></h3><p>Our product appears on customer websites’ in various variations, mainly on login/registration forms, offering a passwordless login option. End users encounter the product as a widget near the password input, along with various prompt variations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/348/1*9AmqgYPJJiqrSDfrZy07Yg.png" /></figure><p>As detailed in my previously published <a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-1-a0f44ca0c55f?source=friends_link&amp;sk=df6acf57392227451224863330261724">article series</a> on product loading time improvement, our product loading relies on a code snippet injected into the customer website. A javascript file with all the necessary resources is loaded by the snippet, containing JSONs of translations and configurations, our SDK code, and other required resources.</p><h3><strong>Chrome API Response Override</strong></h3><p>Thanks to the <a href="https://developer.chrome.com/docs/devtools/overrides/">new feature introduced in Chrome 117</a>, overriding API responses in Chrome is super simple, a developer just needs to follow these steps:</p><p>Right-click on a request in the network tab and choose “Override content.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EQPciP9dgA8EiPQi" /></figure><p>Select the folder to configure the overrides path.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZpJ2Cig-qguzSDpH" /></figure><p>Allow access.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PKfV5K1nXroeQ3HT" /></figure><p>Now you can edit the API response of the selected request. If the selected path is <em>/Users/yedidya/chrome_overrides</em> and the selected request is <em>test.com/x/y.js</em>, then Chrome will automatically generate the folder hierarchy for you: <em>/Users/yedidya/chrome_overrides/test.com/x</em>, and will create a new file under folder <em>x</em>, named <em>y.js</em>, with your customized content.</p><p>Now let’s dive into the challenge, and later we’ll see how Chrome API override is leveraged as part of the solution.</p><h3><strong>The Challenge</strong></h3><p>The loaded javascript file of our product is crafted by a server (our <a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-2-f09340c3bed8?source=friends_link&amp;sk=ec63921679def43bbd4ba3bb9546f6a1">baking-server</a>) that combines many resources from different sources. On the server code, a javascript template with placeholders is created, and placeholders are then replaced with the actual resource.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f7e0e6714113d0bb4ef9e6338c5e96af/href">https://medium.com/media/f7e0e6714113d0bb4ef9e6338c5e96af/href</a></iframe><p>The output is a large javascript file with all relevant resources merged together.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*svNAYfWV7_vXlib2" /><figcaption>baking-server javascript output file</figcaption></figure><p>Reverse engineering this file to understand where the translation JSON is, where the configurations are, and where the SDK code starts or ends is challenging, especially since the SDK code is minified and uglified.</p><h3><strong>Our Goal</strong></h3><p>We aimed to load this javascript file with a different injected SDK code on customer production websites. We wanted the file to contain a specific version of our SDK, which is not minified, not uglified, and can be changed dynamically on-demand — allowing us to examine SDK release candidates on real websites and debug them if needed.</p><h3><strong>The Solution at a High Level</strong></h3><p>The solution involves the following 3 steps; once SDK code is running locally in watch mode and a change is made:</p><ol><li>A script is triggered and downloads the full original javascript file (with translations, configurations, SDK, etc.) from our CDN, for example: <a href="https://cdn.ownid.com/sdk/abcdefg"><em>https://cdn.ownid.com/sdk/abcdefg</em></a></li><li>The SDK code from the downloaded javascript file (uglified and minified) is replaced with the local version of the code (readable and debuggable).</li><li>The new Javascript file with the local SDK code is loaded on the customer’s website instead of the original javascript file.</li></ol><p>Let’s tackle the steps one by one.</p><h3><strong>Clues Planting</strong></h3><p>As I already mentioned, the loaded file in the customer’s website is a large javascript file with all relevant resources merged together. In order to mark the resources so I can tell where every resource is located, I added comments in the baking-server javascript template, before and after each resource injection. This allows me to know which part I need to replace later.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fc904d906a57b5f6babe15ab066cff31/href">https://medium.com/media/fc904d906a57b5f6babe15ab066cff31/href</a></iframe><p>Although the current purpose is to replace the SDK code, I added comments for all injected resources to have the infrastructure to dynamically replace any resource in the future.</p><p>So now, the problem is a bit simpler: Once a customer’s website sends a request to load our product to <a href="https://cdn.ownid.com/sdk/%7Bclient_id%7D,"><em>https://cdn.ownid.com/sdk/{client_id}</em>,</a> I need to interfere with the response and dynamically replace the string that lies between <em>/##WEB_SDK_INJECTION_START##/</em> to <em>/##WEB_SDK_INJECTION_END##/</em> with my local SDK code.</p><p>In the next section, I looked for the point where I can interrupt the code bundling and add some custom steps, to perform the first step from “the solution at a high level” section.</p><h3><strong>SDK Build Process</strong></h3><p>Our SDK is a vanilla JS project packed using the <a href="https://rollupjs.org/">rollup.js JavaScript module bundler.</a> The default configuration that’s being used in the build process before production deployment, performs all classic procedures on the code: minification, uglification, styles adjusting, JSON files to ES6 modules conversion, and more.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a643c870beb5b19a4b5770939e280140/href">https://medium.com/media/a643c870beb5b19a4b5770939e280140/href</a></iframe><p>The command in our package.json that triggers the bundling is:</p><pre>&quot;build&quot;: &quot;rollup -c rollup.config.ts&quot;</pre><p>I decided to leverage rollup.js custom plugin functionality to trigger my custom code and perform the actions I described above, in the “The solution at a high level” section.</p><p>I created a separate rollup.config.ts file with a different name, and a new command to trigger it in the package.json:</p><pre>&quot;start:inject&quot;: &quot;rollup -c rollup-inject.config.ts&quot;</pre><p>I started to investigate the <a href="https://rollupjs.org/plugin-development/">plugin development docs</a>, and the main challenge was to find the right point in rollup.js build lifecycle where I can trigger the plugin so it will run at the right moment, after the SDK code is ready to be injected into the javascript file.</p><p>After investigation, I found <a href="https://rollupjs.org/plugin-development/#output-generation-hooks">Output Generation Hooks</a>, which can provide information about a generated bundle and modify a build once complete. <a href="https://rollupjs.org/plugin-development/#closebundle">closeBundle</a> seemed to be the right point to trigger my code.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8DJdDzBSobLTU0_uBXMA8Q.png" /><figcaption>Output Generation Hooks, from <a href="https://rollupjs.org/">rollupjs.org</a></figcaption></figure><p>The new rollup.js configuration contains my custom plugin. It builds the code without any minification or uglification, to make it readable and debuggable after injection.</p><p>This is the configuration (“injectSdk” is the plugin reference):</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc93ccf61d3b73caaa7b7188c8c25cfc/href">https://medium.com/media/dc93ccf61d3b73caaa7b7188c8c25cfc/href</a></iframe><p>In the next paragraph we’ll dive into the plugin logic, which leverages the Chrome API response override capabilities as part of it.</p><h3><strong>Plugin Logic</strong></h3><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9f96383cfcb95cd11f1cd2a0ef5868c5/href">https://medium.com/media/9f96383cfcb95cd11f1cd2a0ef5868c5/href</a></iframe><p>The code is straightforward, involving the following steps:</p><ol><li>Build the URL of the customer’s javascript file in our CDN, based on parameters from the .env file</li><li>Trigger, in parallel, the javascript file download from the CDN and the local SDK file (that was just built) reading</li><li>Replace the minified and uglified SDK from the original javascript file with the locally readable javascript file.</li><li>Build the local path of the new javascript file on the developer’s machine, based on the URL and Chrome override path preconfigured in Chrome.</li><li>Write the new file to the Chrome override directory.</li></ol><p>Once step 5 is completed, and the javascript file <em>abcdefg</em> exists in the path <em>/Users/yedidya/chrome_overrides/cdn.ownid.com/sdk</em>, Chrome knows to override the network request <em>https://cdn.ownid.com/sdk/abcdefg</em> with the local file. The plugin runs on every change, as rollup.js is configured in watch mode. Building the SDK, downloading the javascript file, replacing, and writing again; these actions are super fast, so in less than a second, as a developer, you can see your code changes take effect on the production website.</p><p>This is a diagram that compares between the regular loading flow and the local SDK loading flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wEOvOd0UdSB6EktE5VPgHg.png" /></figure><h3><strong>Summary</strong></h3><p>This article delves into the creation of a tool utilizing the Chrome API response override and rollup.js custom plugins to load a local version of an SDK for debugging on customer production websites.</p><p>By automating the process, developers can seamlessly make and test code changes on their local machines, reflecting them instantly on relevant customer production websites. While the instructions are tailored to a specific use case, the outlined steps can be applied to enhance any frontend component in various applications.</p><p>Embracing the power of these tools not only streamlines the debugging process but also provides valuable insights into SDK performance on real websites. The combination of Chrome API features and rollup.js custom plugin logic significantly simplifies the workflow, enabling a quick and efficient development cycle.</p><p>Happy coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dd26972c06cb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/automate-to-elevate-debugging-your-local-js-code-seamlessly-on-customers-production-dd26972c06cb">Automate to Elevate: Debugging Your Local JS Code Seamlessly on Customer’s Production</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 3]]></title>
            <link>https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-3-a51f67acc0a1?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/a51f67acc0a1</guid>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[aws-cloudfront]]></category>
            <category><![CDATA[design-patterns]]></category>
            <category><![CDATA[passwordless]]></category>
            <dc:creator><![CDATA[Yedidya Schwartz]]></dc:creator>
            <pubDate>Wed, 19 Apr 2023 11:07:15 GMT</pubDate>
            <atom:updated>2023-05-02T18:23:50.946Z</atom:updated>
            <content:encoded><![CDATA[<h3>[Part 3] Accelerating Load Times: A Materialized View and Server-side Composition Case Study</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bU43hxBEgiVcVu4CveLAkg.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@spencerdavis?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Spencer Davis</a> on <a href="https://unsplash.com/photos/5UeN8VrCxvs?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>In the two previous parts of this series, I introduced the product, the problem, and the design patterns that will be used to solve the problem. I wrote about the objectives that will define the solution as successful. Then, I started to describe the steps taken to apply the design patterns to our architecture. I described Redis as a resources cache layer, the baking-server, and CloudFront as the baked files CDN.</p><p>In this article, the last in the series, I will explain how we verified that the CDN solution works as expected, and then I will explain the last part of the architecture, where Redis pub/sub joins the party.</p><p><em>This is part 3 (and last) of this article series, I highly recommend to first read </em><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-1-a0f44ca0c55f?source=friends_link&amp;sk=df6acf57392227451224863330261724"><em>part 1</em></a><em> and </em><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-2-f09340c3bed8?source=friends_link&amp;sk=ec63921679def43bbd4ba3bb9546f6a1"><em>part 2</em></a><em>.</em></p><h3>How to Verify Our Architecture Stands the Load</h3><p>The main load of requests is handled by the CDN, and our backend doesn’t suffer from any special peaks, even in the rush hours.</p><p>To verify it we used two tools:<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/reports.html"> CloudFront reports in AWS console</a> and <a href="https://www.datadoghq.com/knowledge-center/distributed-tracing/#what-is-distributed-tracing">Datadog Distributed Tracing</a>.</p><p>I chose one baked file of a specific customer for this analysis, and started with checking its statistics with CloudFront reports.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dgwb0uUn3Z0ZHXI6" /><figcaption><strong>Figure 1:</strong> Popular objects page of CloudFront reports in AWS console</figcaption></figure><p>As can be seen in figure 1, the file was requested in the last seven days more than 1.8M times. The “Hits %” column shows the percentage of the requests that were served from the CDN and were not sent to the origin (baking-server). As we can see, 99.88% is a good result for cache hits — only 253 misses. This means that in one week, the baking-server had 253 requests for baking this specific file.</p><p>To make an extra check, I used our observability tool, Datadog, which collects data of any performed request on our backend components, to see how many requests were performed to the baking-server for this specific customer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X98eQBNi4qIzIJiZ" /><figcaption><strong>Figure 2:</strong> Datadog traces, filtered on baking-server for a specific file</figcaption></figure><p>Indeed, very few requests were performed to the specific file in the same time window, and the amount is similar to the data that appeared on the CloudFront report: 249/253.</p><p>So, approximately 250 requests to the baking server compared to almost 2M requests in total.</p><p>This is exactly the verification we wanted to get, so we are good to keep on, to requirement number 4 from the previous article: the response content should always be with the most updated data.</p><h3>Redis Pub/Sub as Invalidation Trigger</h3><p>The last step of making the baking mechanism work properly is to handle the CDN invalidation logic.</p><p>Each time the SDK code or the localization files are updated, the CDN needs to clear from its edge locations the current versions of <strong>all</strong> customer’s baked files, so the next request to the file will fallback to the origin — the baking server — and will re-bake everything with the most updated data.</p><p>Each time a customer updates their own app settings in the management console, the CDN needs to clear from its edge locations only the specific file of the customer, so the next request to the file will re-bake the new settings to the file, using the baking server.</p><p>Actually, the invalidation processes can be split into two types: a global invalidation and an individual invalidation. To implement this functionality, we chose an event-driven approach, using <a href="https://redis.io/docs/manual/pubsub/">Redis pub/sub</a>.</p><ul><li><strong>For the global invalidation: </strong>The CD processes of our SDK and localization projects are publishing a “global invalidation” event. The config-server is a subscriber of this event, and once the event is received, the server sends an invalidation request to CloudFront, for the following pattern: “/sdk/*”, to clear all baked files.</li><li><strong>For the individual invalidation: </strong>Once a customer updates their app settings on the management console, an “individual invalidation” event is triggered, with the customer’s App ID as a variable. The config-server is a subscriber of this event as well, and once the event is received, the server sends an invalidation request to CloudFront, for the following pattern: “/sdk/&lt;app_id&gt;”, to clear only the specific apps’ baked file from the edge locations.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*7qWmHOMNSuWxmkHIlelcXg.png" /><figcaption><strong>Figure 3:</strong> The architecture with the Redis pub/sub component, to handle CDN invalidations</figcaption></figure><p>This part is a suitable solution for requirement #4: the response content should always be with the most updated data, with the last settings that were configured by the customer in the management console, with not more than one to two minutes of delay. (For example, if the widget color was changed from red to green, in max two minutes all end-users will start seeing a green widget.)</p><p>So now that we’ve marked all the required bullets, let’s test the result.</p><h3>The Result</h3><p>After the solution is ready and works e2e, we can test the performance of our widget loading using the baked file. In order to test it, I updated the settings of one app in the management console, to invalidate its baked file from the CDN, and then loaded a page where the widget exists.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Sxl99dp5L-I_31wu" /><figcaption><strong>Figure 4:</strong> Network tab screenshot of the request being performed a few times for the widget load after the fix</figcaption></figure><p>As can be seen in figure 4, I loaded the page four times to see the latency of a few identical requests. The first request took almost 200ms, as the required baked file didn’t exist on the CDN yet, and the baking-server had to create the file.</p><p>The next requests took 31, 14 and 17 ms, much faster than the first request, as the files are served from the CDN edge locations.</p><p>And if we compare this latency to the latency of the widget loading before the architectural change, the change is phenomenal: from more than 0.5 seconds to 15ms. An improvement of hundreds of percent in the latency.</p><p>The architectural change I described in these three articles significantly improved the experience of our widget’s loading on our customer’s websites. We gradually migrated our customers to use the new mechanism, to mitigate the risk in case we found any critical bugs.</p><p>After a few weeks of using it, we can also verify the quality of the solution, as we haven’t yet experienced any performance issues.</p><h3>Summary</h3><p>In this article series I described the main implementation steps we took to boost the product loading performance. The solution was inspired by two known design patterns: the Materialized View and the Server-Side Composition. The solution incorporated aspects of the both design patterns: gathering complex data from different sources for storing it in one place for an efficient querying, as well as pre-building the UI on the backend, rather than piecing together multiple sources on the frontend.</p><p>We started with Redis as a resources cache layer on the backend, to keep the files close network-wise. This saved us from requesting the resources from S3 or other servers, an action that would increase the required time to read them.</p><p>Then I explained about the baking server — the core service of the architecture — who is responsible for reading all the required resources and merging them into one JS file.</p><p>The next step was the CDN, which acts as the baked files cache layer. I mentioned two cool behaviors of CloudFront — requests collapsing and origin shield — and I explained how to verify that the CDN acts as expected, using Datadog and CloudFront reports in the AWS console.</p><p>Lastly, I introduced Redis pub/sub, which is responsible for orchestrating the CDN invalidation events, to make sure the served files are always the most updated ones.</p><p>Before and even during the implementation, we had a lot of questions about the best architectural decisions to make, in many parts of the solution. Some examples are:</p><ul><li>Where should we trigger the CDN invalidation event?</li><li>Should the baking-server store the baked files in S3 to prevent overload of the server, or the CDN is good enough as the cache layer?</li><li>If we go with the S3 approach, there’s a huge overhead when we need to invalidate all files and upload them all at once to S3. Is this heavy operation worth it?</li><li>Should we use an extra cache layer, in addition to Redis, and store the resources in an in-memory cache in the baking server?</li><li>And many more…</li></ul><p>In the end, we believe we’ve made the right decisions under the circumstances of our existing architecture. One thing I can tell for sure: the thinking process was very interesting. The main conclusion is that even when you are 100% sure you have the full solution in a diagram before you start with the implementation, you’ll have some changes during the work. You just need to hope they won’t completely devour all the cards…</p><p>Did you learn something new from our use case? <a href="https://www.linkedin.com/in/yedidyas/">Feel free to share your thoughts and questions.</a></p><p>You are welcomed to <a href="https://medium.com/ownid-engineering/newsletters/ownid-engineering">subscribe</a> to OwnID Engineering newsletter to get updates when new articles are published.</p><h4>Fan fact</h4><p>My Slack custom emoji since the Baking Server was released to production (thanks <a href="https://medium.com/u/11885880f6b3">Niv Navick</a>):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/163/1*HDRGxBCUal8eudFlDZVrOw.png" /></figure><h3>Sources</h3><p><strong>Materialized view pattern</strong></p><p><a href="https://cloud.google.com/bigquery/docs/materialized-views-intro#:~:text=Queries%20that%20use%20materialized%20views,of%20common%20and%20repeated%20queries">https://cloud.google.com/bigquery/docs/materialized-views-intro#:~:text=Queries%20that%20use%20materialized%20views,of%20common%20and%20repeated%20queries</a>.</p><p><a href="https://www.educative.io/blog/materialized-view-introduction">https://www.educative.io/blog/materialized-view-introduction</a></p><p><a href="https://medium.com/event-driven-utopia/querying-microservices-with-the-cqrs-and-materialized-view-pattern-bdb8b17f95d1">https://medium.com/event-driven-utopia/querying-microservices-with-the-cqrs-and-materialized-view-pattern-bdb8b17f95d1</a></p><p><a href="http://www.jeisystems.co.uk/tech-blog/programming-blog/materialized-view-pattern/">http://www.jeisystems.co.uk/tech-blog/programming-blog/materialized-view-pattern/</a></p><p><a href="https://www.waytoeasylearn.com/learn/materialized-view-pattern/">https://www.waytoeasylearn.com/learn/materialized-view-pattern/</a></p><p><a href="https://devdojo.com/bobbyiliev/decoupled-microservices-architecture-with-materialize">https://devdojo.com/bobbyiliev/decoupled-microservices-architecture-with-materialize</a></p><p><a href="https://docs.oracle.com/cd/B10500_01/server.920/a96567/repmview.htm">https://docs.oracle.com/cd/B10500_01/server.920/a96567/repmview.htm</a></p><p><strong>UI Composition</strong></p><p><a href="https://samnewman.io/patterns/architectural/ui-composition/#:~:text=With%20UI%20composition%2C%20what%20appears,be%20managed%20by%20different%20teams">https://samnewman.io/patterns/architectural/ui-composition/#:~:text=With%20UI%20composition%2C%20what%20appears,be%20managed%20by%20different%20teams</a></p><p><a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/microservice-based-composite-ui-shape-layout">https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/microservice-based-composite-ui-shape-layout</a></p><p><a href="https://particular.net/blog/secret-of-better-ui-composition">https://particular.net/blog/secret-of-better-ui-composition</a></p><p><a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/microservice-based-composite-ui-shape-layout">https://learn.microsoft.com/en-us/dotnet/architecture/microservices/architect-microservice-container-applications/microservice-based-composite-ui-shape-layout</a></p><p><strong>Server-side composition</strong></p><p><a href="https://microservices.io/patterns/ui/server-side-page-fragment-composition.html">https://microservices.io/patterns/ui/server-side-page-fragment-composition.html</a></p><p><a href="https://livebook.manning.com/book/micro-frontends-in-action/chapter-4/7">https://livebook.manning.com/book/micro-frontends-in-action/chapter-4/7</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a51f67acc0a1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-3-a51f67acc0a1">Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 3</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 2]]></title>
            <link>https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-2-f09340c3bed8?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/f09340c3bed8</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[passwordless]]></category>
            <category><![CDATA[aws-cloudfront]]></category>
            <category><![CDATA[design-patterns]]></category>
            <category><![CDATA[architecture]]></category>
            <dc:creator><![CDATA[Yedidya Schwartz]]></dc:creator>
            <pubDate>Tue, 04 Apr 2023 13:27:29 GMT</pubDate>
            <atom:updated>2023-08-08T11:28:47.239Z</atom:updated>
            <content:encoded><![CDATA[<h3>[Part 2] Accelerating Load Times: A Materialized View and Server-side Composition Case Study</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RD67x4rYB1oZtS4l5enT2g.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/es/@picoftasty?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Mae Mu</a> on <a href="https://unsplash.com/photos/rgRbqFweGF0?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p><a href="https://medium.com/@yedidya.s/a0f44ca0c55f?source=friends_link&amp;sk=df6acf57392227451224863330261724">In the previous part</a>, I introduced the product, mapped the problem, and detailed the design patterns that will be used to solve the problem: Materialized View and Server-side Composition.</p><p>In this part, I will map five objectives that are required from the solution, and then I’ll go over them, step by step, showing you how we built the full solution. At each step, I will show you how the objective is expressed in the architecture. I will explore Redis as a resource caching layer, discuss the essential logic contained in the baking-server, and highlight the impressive capabilities of CloudFront as a CDN, including some of its most intriguing behaviors.</p><h3>Our Requirements for the Solution</h3><p>In part one, I presented the current approach of the widget loading, and the main idea of the applied patterns. Now, let’s list the five required technical aspects:</p><ol><li>Merge the four network requests into one request that will contain all four requests content.</li><li>That single request latency will be very very low, as close to zero milliseconds as possible (i.e., the request should be heavily cached).</li><li>If the requested content isn’t ready (i.e, baked) yet, it should be still accessible in real time, in not more than one second.</li><li>The response content should always be with the most updated data, with the last settings that were configured by the customer in the management console. Max one to two minutes of delay are acceptable (e.g., if the widget color was changed from red to green, in max two minutes all end-users will start seeing a green widget).</li><li>The baked files should support a high scale of requests, tens of thousands per second in the peak time.</li></ol><p>Accomplishing these five objectives is a must to declare the mission a success. The main challenge is to examine how to apply them to our existing architecture in such a way that won’t require massive changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/891/1*fuf_K9Flogi2lgyC1p7DFg.png" /><figcaption><strong>Figure 1:</strong> The architecture of widget loading flow before the change</figcaption></figure><p>Furthermore, we can’t risk the existing widget loading approach, as we should support backward compatibility for customers until they migrate to the new solution.</p><p>As I explained in part one, the existing widget loading process contains an individual network request per resource. Let’s start to improve it.</p><h3>Redis as a Resources Cache Layer</h3><p>As you can see in figure 1, the resources are fetched from S3 and from the multi-tenant server. So as the first step, to optimize the baking process that we are about to develop, we want to make sure that every required resource will be cached in <a href="https://redis.io/">Redis</a>. In this way, the <strong>baking-server</strong> (described in the next paragraph) will be able to fetch the content it needs to bake in the fastest possible way: from the Redis cache, instead of downloading it from S3, or performing an HTTP request to other services, which are actions that will be much slower.</p><p>A separate service — <strong>config-server</strong> — is the responsible service to load the resources into Redis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*vsbOvHR1KZyFM_REraQBww.png" /><figcaption><strong>Figure 2</strong>: The architecture with the addition of Redis as the resources cache, and config-server, the responsible service for reading the resources from their origins and writing them into Redis</figcaption></figure><p>This part is a suitable solution for requirement #3: If the requested content isn’t ready (i.e, baked) yet, it should still be accessible in real time, in not more than one second. Well, as I will describe in the next paragraph, the resources will be accessible to the <strong>baking-server</strong> from a Redis cache layer, without the need of reading it from S3 or other services, so reading all resources shouldn’t take more than a few tens of ms.</p><p>The next step in the architecture is the <strong>baking-server</strong>, the responsible service for reading all cached resources and baking them into one file that will be served to the end users.</p><h3>Baking Server</h3><p>As the solution core, we created a new microservice that owns the baking business. This service has a well defined responsibility: application ID as an input, and a JS file, containing all the required resources as an output.</p><p>The logic is very simple: the baking-server reads the relevant resources from Redis — that always holds their most updated versions — and replaces the relevant place-holders in a pre-defined JS structure, that was agreed with our frontend side.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1130ea25285aa71331b3dbda559b2526/href">https://medium.com/media/1130ea25285aa71331b3dbda559b2526/href</a></iframe><p>An example of a request to the service:</p><p><a href="https://cdn.ownid.com/sdk/jflb8atm2yywr3">https://cdn.ownid.com/sdk/jflb8atm2yywr3</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*PfT4Pf9uJPs-bFKLv96xlQ.png" /><figcaption><strong>Figure 3: </strong>The architecture with the addition of the Baking Server, that has the core logic of the solution</figcaption></figure><p>This part is a suitable solution for requirement #1: merge the four network requests to one request that will contain all four requests content.</p><p>Now, we need to make sure that the baking-server won’t suffer on a traffic peak time; as our customers may have tens of thousands of visitors on their websites, we don’t want our servers to crash under the pressure.</p><h3>CDN as the Baked Files Cache Layer</h3><p>As mentioned, the next part of the architecture that I will explain about is the <a href="https://en.wikipedia.org/wiki/Content_delivery_network">CDN</a> between the end-user and the baking server.</p><p>The strategy we chose is pre-baking, but nevertheless, it has an on-demand aspect for the first end-user that accesses that file. Only on the first end-user’s request, the baking-server logic is triggered.</p><p>The first request to a file that’s performed by some of the end-users reaches to the baking-server, but the next end-users who will request the file will get it from the CDN.</p><p>We are using <a href="https://www.amazonaws.cn/en/cloudfront/">AWS CloudFront</a> as CDN. Each baked file is stored on the <a href="https://aws.amazon.com/cloudfront/features/?whats-new-cloudfront.sort-by=item.additionalFields.postDateTime&amp;whats-new-cloudfront.sort-order=desc">edge locations</a>, and that’s protecting the baking-server (also known as the “Origin”) from getting tons of files baking requests.</p><p>To be more accurate: for each one of our customer’s websites, only one end-user per edge location (e.g: one end-user from Chicago, one end-user from London and one from Prague, etc…) will be the one who triggers the baking-server action, and only these end-users will experience the slower loading of the widget.</p><p>After they are experiencing the fallback from the CDN to the baking-server, the baked file is stored in the CDN edge location, and from that point it will be served directly from the edge location, without the need of the baking-server, in an average time of 15ms.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/976/1*rPZT90AymppLoUySVzQNtw.png" /><figcaption><strong>Figure 4:</strong> The architecture with the addition of a CDN in front of the Baking Server</figcaption></figure><p>This part is a suitable solution for requirement #2 and #5: the single request latency will be very very low, as far to 0 ms as possible, and the baked files should support a high scale of requests, tens of thousands per second in the peak time.</p><h3>The Power of CloudFront as a CDN</h3><p>I would like to share with you 2 interesting concepts and behaviors of CloudFront that I found very useful.</p><h4>Requests Collapsing</h4><p>Let’s assume that during the rush hour of the day, a customer performs an update on our management console, and changes some widget configuration. The “save” action will <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide//Invalidation.html">invalidate</a> the baked file from the CDN (in the next article in the series I will explain the invalidation process in depth), so the next request that will ask to get the file will fallback to the origin — the baking-server.</p><p>Now, as it is rush hour, there may be at the same second thousands of end-users that request the file in parallel. Since the file doesn’t exist in the CDN, you would think that the baking-server would get all those thousands of requests, and this would result in a huge load that may cause issues to our backend infrastructure.</p><p>Exactly for this edge case, AWS introduced the <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html#request-custom-traffic-spikes">request collapsing feature</a> as part of the default behavior of CloudFront:</p><blockquote><em>When a CloudFront edge location receives a request for an object and the object isn’t in the cache or the cached object is expired, CloudFront immediately sends the request to the origin. However, if there are simultaneous requests for the same object — that is, if additional requests for the same object (with the same cache key) arrive at the edge location before CloudFront receives the response to the first request — </em><strong><em>CloudFront pauses before forwarding the additional requests to the origin. This brief pause helps to reduce the load on the origin. CloudFront sends the response from the original request to all the requests that it received while it was paused.</em></strong><em> This is called request collapsing.</em></blockquote><p>(AWS documentation)</p><h4>Origin Shield</h4><p>Another useful feature I want to mention is the <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html">Origin Shield</a>. Although we didn’t use it, it’s important to be aware of this option, in case your origin is getting too many requests and you would like to reduce them.</p><p>The following paragraph from <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html">AWS documentation</a> explains the basics of CloudFront architecture:</p><blockquote><em>With Amazon CloudFront, you inherently get a reduced load on your origin because requests that CloudFront can serve from the cache don’t go to your origin. In addition to CloudFront’s global network of edge locations, regional edge caches serve as a mid-tier caching layer to provide cache hits and consolidate origin requests for viewers in nearby geographical regions. Viewer requests are routed first to a nearby CloudFront edge location, and if the object isn’t cached in that location, the request is sent on to a regional edge cache.</em></blockquote><p>(AWS documentation)</p><p>As you can understand — the basic CloudFront architecture contains 2 cache levels: edge locations and regional edges. Later in the documentation, you can read how Origin Shield can give you extra protection on your origin, by adding a third cache layer:</p><blockquote><em>When viewers are in different geographical regions, requests can be routed through different regional edge caches, </em><strong><em>each of which can send a request to your origin for the same content.</em></strong><em> But with Origin Shield, </em><strong><em>you get an additional layer of caching</em></strong><em> </em><strong><em>between the regional edge caches and your origin</em></strong><em>. All requests from all regional edge caches go through Origin Shield, further reducing the load on your origin.</em></blockquote><p>(AWS documentation)</p><p>In the following diagram, you can see the fully described CloudFront architecture, with Origin Shield (“AWS Elemental MediaPackage” is the origin — i.e, baking-server in our use case):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xRbDb1RVSsq1wrng" /><figcaption><strong>Figure 5:</strong> CloudFront architecture with Origin Shield (Diagram by AWS)</figcaption></figure><h3>Summary</h3><p>In this part of the series I wrote about the objectives that will define the solution as successful. Then, I started to describe the steps taken to apply the design patterns to our architecture.</p><p>I started with Redis as a resources cache layer, and then I presented the baking-server, which owns the core logic of the solution. In the last paragraph, I introduced the power of CloudFront as a CDN, and some of its most interesting behaviors.</p><p>In the next article — which will be the last in this series — I’ll explain how we verified that the CDN solution works as expected, and then I will add the last missing part of the architecture — Redis pub/sub as invalidation trigger.</p><p><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-3-a51f67acc0a1?source=friends_link&amp;sk=4f272c49f6eacde38ba7fbfe1b4afdc7">Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 3</a></p><p>You are welcomed to <a href="https://medium.com/ownid-engineering/newsletters/ownid-engineering">subscribe</a> to OwnID Engineering newsletter to get updated when the next part is published.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f09340c3bed8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-2-f09340c3bed8">Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 2</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 1]]></title>
            <link>https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-1-a0f44ca0c55f?source=rss----5dc036f5f73e---4</link>
            <guid isPermaLink="false">https://medium.com/p/a0f44ca0c55f</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[passwordless]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[design-patterns]]></category>
            <dc:creator><![CDATA[Yedidya Schwartz]]></dc:creator>
            <pubDate>Wed, 22 Mar 2023 17:49:22 GMT</pubDate>
            <atom:updated>2023-05-02T18:24:47.258Z</atom:updated>
            <content:encoded><![CDATA[<h3>[Part 1] Accelerating Load Times: A Materialized View and Server-side Composition Case Study</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7gVu972f0aShYNN8QBCfOA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@stefanasafti?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Stefan C. Asafti</a> on <a href="https://unsplash.com/photos/x5jilo3ck3o?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><p>How did OwnID improve the loading of the product by hundreds of percent? Read about our architectural change implementation details, and learn how we leveraged practical design patterns using Redis, AWS Cloudfront, microservices, and observability tools to get top-level performance.</p><h3>Introduction</h3><p>You can find on the web many articles about <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/materialized-view">Materialized View pattern</a>, and a lot of praise for the way it can boost your application performance. But most of the references are for data-oriented use cases, on the DB level, and very few of them tell about files baking for a static site. Looks like you can’t find any that describe a use case of baking static files together with data for multi-tenant systems.</p><p>Same thing with <a href="https://microservices.io/patterns/ui/server-side-page-fragment-composition.html">Server-side Composition pattern</a>: all the written use cases I could find for this pattern focus on the static side aspect of the website; it is proposed as an alternative for loading micro-frontends separately on the browser. No use case focuses on a composition of static files together with data for multi-tenant systems while focusing on the data load performance aspect.</p><p>In the following three articles, I describe how using the combination of both patterns — server-side composition and materialized view — helped us to improve, by hundreds of percent, the loading time of our product. I will present the process we went through step by step, using detailed diagrams of the main changes we made in our architecture to apply the patterns, while using advanced features of Redis, AWS CloudFront CDN, and observability tools.</p><h3>The Widget Loading Experience</h3><p><a href="http://ownid.com/">OwnID</a> provides a seamless, web-based authentication solution that <a href="https://ownid.com/blog/the-impact-on-user-experience-and-conversion-rates/?domain_trace=ownid.com">eliminates passwords</a>. With just one button, users can authenticate effortlessly — and without needing to install an app. The solution blends seamlessly into existing forms, and works cross-OS, cross-device, and cross-domain, ensuring maximum security and a frictionless user experience.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ddLT_Ix9RpQqd5cW" /><figcaption><strong>Figure 1</strong>:<strong> </strong>A login form with OwnID’s widget</figcaption></figure><p>In order to integrate OwnID’s widget on a website, a developer needs to add a code snippet on the frontend, and configure a few values in its <a href="https://console.ownid.com/">OwnID’s Management Console</a> account. OwnID will communicate with the website <a href="https://en.wikipedia.org/wiki/Identity_provider">IDP</a>, whether it’s a third-party tool or an in-house solution, without storing any end-users data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dd3KrPtTJ_N6emMU" /><figcaption><strong>Figure 2:</strong> OwnID’s management console</figcaption></figure><p>OwnID’s architecture is multi-tenant based, which means that all the customer’s configurations are stored in one DB, and for each customer’s app (i.e., website) the widget is loaded with its specific pre-configured settings — for example, tooltip colors, positioning (left/right to the password field), IDP integration, translations, feature flags, and more.</p><p>No doubt that the widget loading experience must be as smooth as possible, so that it feels like it’s a native component of the containing website.</p><p>This is exactly the pain point that made us come up with this improvement process. We knew there was a lot of room for improvement in this domain, so we thought <em>let’s see what we can do about it</em>.</p><h3>How the Widget Loaded Before the Change</h3><p>Before the architectural change we made, in order to load our widget we used to send four different network requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1lzXpvt_idLPj4Fz" /><figcaption><strong>Figure 3:</strong> Network tab screenshot of the requests being performed for the widget load</figcaption></figure><p>firebase.sdk.js — SDK file — used to integrate with a specific pre-configured IDP, Firebase in this case.</p><ol><li>client-config — get various settings of the customer’s widget.</li><li>langs.json — list of supported languages.</li><li>web-sdk.json — key/value mapping of the SDK translations for a specific language.</li></ol><p>As you can see, each request may have pretty high latency, and some of them were dependent on each other.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/891/1*3aL2xAaY8bVUURUTUWKqzw.png" /><figcaption><strong>Figure 4:</strong> The architecture of widget loading flow before the change</figcaption></figure><p>The problems with this approach are:</p><ol><li>There are four different network requests as a precondition for the widget loading, and each of the requests is sent to a different component, which increases the chance of a glitch.</li><li>There are dependencies between some of the requests.</li><li>There may be a small delay in the widget appearance, which from time to time may appear a few milliseconds after the website’s login form is loaded. This is not the ideal user experience.</li></ol><p>We could probably “patch” these issues and get some loading time improvement by applying and adjusting some caching techniques. We could also add some fallback hard-coded values as a backup in case of network failures.</p><p>But since we wanted to achieve the best user experience, together with the most reliable loaded data, we chose to perform the most comprehensive solution we could come up with.</p><h3>The Materialized View Pattern</h3><blockquote>In computing, a materialized view is a database object that contains the results of a query. For example, it may be a local copy of data located remotely, or it may be a subset of the rows and/or columns of a table or join result, or a summary using an aggregate function.</blockquote><p>— “<a href="https://en.wikipedia.org/wiki/Materialized_view">Materialized view</a>”, Wikipedia,</p><p>The Materialization Pattern can be implemented on various levels of the architecture, and at different levels of the data flow. Moreover, “a database object”, as Wikipedia describes it, can be stored on many types of databases: a document DB like MongoDB or Redis, a SQL DB as PostgreSQL, and even a distributed DB as CDN.</p><p>For example, the pattern can be implemented <a href="https://medium.com/event-driven-utopia/querying-microservices-with-the-cqrs-and-materialized-view-pattern-bdb8b17f95d1">on the DB level</a>, for data only, in a pre-calculated column that contains the result of a high-complexity join query from multiple columns.</p><p>A different approach is to implement the pattern on the UI level: <a href="https://simonwillison.net/2021/Jul/28/baked-data/">unify the static website data with the JS code resources</a>, and store the merged content in a CDN.</p><p>The implementation level of this pattern in the data flow is flexible, as is the storage of the materialized output. The flow’s step that triggers the materialization action is very flexible too: on one hand, you can perform the preparation in advance for all possible content combinations; on the other hand, you can wait for the end user to access the relevant data and only then trigger the materialization process, to perform it in “real-time.”</p><p>The bottom line? The pattern has a lot of room for flexibility: when to trigger the baking process, how to read the content, where to store it, how to refresh it, and many more concerns. As you can tell about any architecture: there’s no right and wrong decision; you need to fit the pattern to your own use case.</p><p>From my reading, I found that on the UI level, the materialization approach is better known as “baking” and not “materialization.” In the following articles I will use the word “baking” to describe materialization. As I see it, the baking approach has a lot in common with the second pattern I’m about to describe: the server-side composition.</p><h3>The Server-side Composition Pattern</h3><p>This design pattern solves the challenge of displaying data from multiple services on one screen, and loading that screen in an efficient way.</p><p>As <a href="https://microservices.io/patterns/ui/server-side-page-fragment-composition.html">Chris Richardson</a> explains:</p><blockquote>Each team develops a web application that generates the HTML fragment that implements the region of the page for their service. A UI team is responsible for developing the page templates that build pages by performing server-side aggregation (e.g. server-side include style mechanism) of the service-specific HTML fragments.</blockquote><p>This is a similar approach to the <a href="https://microservices.io/patterns/ui/client-side-ui-composition.html">Client-side UI composition pattern</a>: same idea, but on the server side.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*D_5LonFj06mAEBVJ" /><figcaption><strong>Figure 5:</strong> Server-side composition Pattern (Diagram by Michael Geers, in “Micro Frontends in Action”)</figcaption></figure><p>Common tools for applying this pattern are <a href="http://nginx.org/en/docs/http/ngx_http_ssi_module.html">Nginx and Server-Side Includes (SSI</a>) or <a href="https://podium-lib.io/">Podium</a>. Using these tools, you can perform request forwarding to several services, and then merge their responses into one structure and return them as one “baked” response.</p><p>These tools alone don’t promise that the accessed data on the backend side will be accessible in the most efficient way; the servers may access data using a slow DB query, which will result in a very slow response to the browser.</p><p>This pattern only focuses on the backend components’ composition and their merged output. The efficiency of accessing the data, which isn’t covered by this pattern, is exactly where the Materialized View pattern which was described in the previous paragraph can complete the picture, and close the gap.</p><p>In the following articles, I will walk you through the thinking journey we had during our implementation of these patterns. To be honest, I’m not sure which one of the patterns better represents the approach of the solution I’m about to describe. Our solution takes some advantages from each one of the patterns, so I decided to present both. I hope this case study will give you some points to think about if you are looking for a way to boost your application performance.</p><h3>Summary</h3><p>That’s it for the first part of the “Accelerating Load Times” article series. In this part I introduced our product, I mapped the problem, and I laid out the design patterns that will be used to solve the problem. In the next parts of the series, we’ll see in detail how to map the problem into action items, and how the explained design patterns are translated into the wanted architectural change.</p><p><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-2-f09340c3bed8?source=friends_link&amp;sk=ec63921679def43bbd4ba3bb9546f6a1">Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 2</a></p><p>You are welcomed to <a href="https://medium.com/ownid-engineering/newsletters/ownid-engineering">subscribe</a> to OwnID Engineering newsletter to get updates when new articles are published.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a0f44ca0c55f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/ownid-engineering/accelerating-load-times-a-materialized-view-and-server-side-composition-case-study-part-1-a0f44ca0c55f">Accelerating Load Times: A Materialized View and Server-side Composition Case Study — Part 1</a> was originally published in <a href="https://medium.com/ownid-engineering">OwnID Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>