<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CoinGecko Engineering</title>
    <description>The latest articles on DEV Community by CoinGecko Engineering (@coingecko).</description>
    <link>https://dev.to/coingecko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F6409%2F79beaf34-1b0d-4323-92be-90965afae54e.png</url>
      <title>DEV Community: CoinGecko Engineering</title>
      <link>https://dev.to/coingecko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coingecko"/>
    <language>en</language>
    <item>
      <title>Scaling PostgreSQL Performance with Table Partitioning</title>
      <dc:creator>Amree Zaid</dc:creator>
      <pubDate>Fri, 13 Jun 2025 01:25:06 +0000</pubDate>
      <link>https://dev.to/coingecko/scaling-postgresql-performance-with-table-partitioning-136o</link>
      <guid>https://dev.to/coingecko/scaling-postgresql-performance-with-table-partitioning-136o</guid>
      <description>&lt;p&gt;Table of contents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The background&lt;/li&gt;
&lt;li&gt;The investigation&lt;/li&gt;
&lt;li&gt;The execution&lt;/li&gt;
&lt;li&gt;The result&lt;/li&gt;
&lt;li&gt;What we would do differently&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The background
&lt;/h2&gt;

&lt;p&gt;In CoinGecko we have multiple tables that we use to store crypto prices for various purposes. However, after over 8 years of data, one of the tables we store hourly data in grew over 1TB to a point it took over 30 seconds on average to query.&lt;/p&gt;

&lt;p&gt;We started to see higher IOPS usage whenever there are more requests hitting price endpoints. Requests queues started to increase and our Apdex score started to go down. For a short term fix, we increased the IOPS up to 24K. However, the IOPS keeps on getting breached causing alerts everyday.&lt;/p&gt;

&lt;p&gt;In order to ensure this situation doesn’t affect our SLO and eventually our SLA, we started to look into what we can do to improve the situation.&lt;/p&gt;

&lt;p&gt;FYI, we are using PostgreSQL RDS as our main database.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;Adding indexes was initially considered the quickest solution, but this approach was unsuccessful. The query utilizes a JSONB column with keys based on supported currencies, presenting an additional challenge. Indexing different keys for various applications was deemed excessive, as an added index might only benefit a single application.&lt;/p&gt;

&lt;p&gt;Ultimately, table partitioning was chosen as the solution most likely to yield the greatest returns, despite its complexity.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is table partitioning?
&lt;/h3&gt;

&lt;p&gt;Table partitioning involves dividing a large table into smaller, more manageable pieces called partitions. These partitions share the same logical structure as the original table but are physically stored as separate tables.&lt;/p&gt;

&lt;p&gt;This allows queries to operate on only the relevant partitions, improving performance by reducing the amount of data scanned.&lt;/p&gt;

&lt;p&gt;There are three methods of partitions which are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Range:&lt;/strong&gt; Partitions data based on a range of values (e.g., dates, numerical ranges).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List:&lt;/strong&gt; Partitions data based on specific list values (e.g., countries, categories).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash:&lt;/strong&gt; Partitions data by applying a hash function to a column's value, distributing data evenly across partitions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The question is, which partitioning method should we use? As usual, the answer is: it depends. We need to analyze our query patterns to determine which method will provide the greatest benefit. The key is to select the method that minimizes the amount of data our queries need to read every time it runs.&lt;/p&gt;

&lt;p&gt;In our case, range partitioning is the optimal choice. This is due to the fact that almost all of our queries on this table incorporate a timestamp range in the &lt;code&gt;WHERE&lt;/code&gt; clause. Moreover, we know that we generally only require data for a few months at a time, with a maximum of four. As a result, partitioning the table by month will guarantee that our queries only access up to four partitions (most of the time).&lt;/p&gt;

&lt;p&gt;IF for some reason, we are not limiting the read based on the timestamp, we may need to use the Hash method as that will limit the read based on a foreign key. Again, it depends on the use case.&lt;/p&gt;

&lt;p&gt;What would the code look like?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create partitions for different amount ranges&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_small&lt;/span&gt;
   &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
   &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_medium&lt;/span&gt;
   &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
   &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_large&lt;/span&gt;
   &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
   &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_extra_large&lt;/span&gt;
   &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
   &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAXVALUE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert sample data&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;-- Goes to orders_small&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;-- Goes to orders_medium&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;-- Goes to orders_large&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- Goes to orders_extra_large&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;-- Goes to orders_small&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;450&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;-- Goes to orders_medium&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, I just name the partition based on this format: table_YYYYMM.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What did I learn during the investigation?
&lt;/h3&gt;

&lt;p&gt;If someone had to do this again, these are the info that I will pass on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We can’t change the table from an unpartitioned table to a partitioned table. We need to create a new table and copy the data into it before making the switch.&lt;/li&gt;
&lt;li&gt;The partitioned table needs to have the partition key as part of the primary key. If we have ID as the original primary key, then, we need to use composite keys on the new table.&lt;/li&gt;
&lt;li&gt;In order to use partitioned tables in Ruby on Rails, we need to change the schema format from &lt;code&gt;schema.rb&lt;/code&gt; to &lt;code&gt;schema.sql&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We need to figure out how to have both tables running at the same time if we can’t afford downtime.&lt;/li&gt;
&lt;li&gt;Since we will be creating a new table, we have to be careful about the cache. Technically, the new table doesn’t have cache at all and the performance will be very bad. We have to figure out how to “warm” up the new table. I am referring to the cache in PostgreSQL itself.&lt;/li&gt;
&lt;li&gt;To warm up the table, please learn about pg_prewarm.&lt;/li&gt;
&lt;li&gt;Copying data as big as 1.2TB would require bigger resources such as IOPS. We need to take that into account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With all the info that we had, we created a Release Plan document outlining when and what is going to happen. We used that document as our main reference point for everyone to see. The document contains these info:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date:&lt;/strong&gt; When it is going to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prerequisites:&lt;/strong&gt; Before executing the todos, we may need to do other tasks first. They will be listed in this section ensuring we do not start the todos without completing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risks:&lt;/strong&gt; For every risk, we will list down what could happen and what are the mitigation plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todos:&lt;/strong&gt; This section will list down what needs to be done and once it is done, we will tick them off from the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The execution
&lt;/h2&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dry Run
&lt;/h3&gt;

&lt;p&gt;Our customers are very particular about the uptime of our services, hence, we proactively conduct dry runs to safeguard our SLO/SLA commitments. Before we start our dry run, we will list down what we want to do and what kind of statistics that we want to collect.&lt;/p&gt;

&lt;p&gt;For this project, we spinned up another database identical to our production. Then, we ran all the commands or scripts that we will run on the production instance later. In our case, we were looking for these data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before and after query performance.&lt;/li&gt;
&lt;li&gt;How long will it actually take to copy the data?&lt;/li&gt;
&lt;li&gt;How long does it take to warm up the table partitions?&lt;/li&gt;
&lt;li&gt;What does the CPU and IOPS look like for every action that we did?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoid operating in the dark.. Be prepared so that we won’t miss our objectives. Remember, using a database similar to our production is going to cost a lot of money.&lt;/p&gt;

&lt;p&gt;What we found out during the dry run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The original table was too slow at first. But, this is expected as without any cache, we won’t be able to work on it.&lt;/li&gt;
&lt;li&gt;It took us 10 hours to just warm up the original table before we could start trying out our commands.&lt;/li&gt;
&lt;li&gt;It took us 3 days+ to finish copying the data.&lt;/li&gt;
&lt;li&gt;Total IOPS can spike up to 6,000 during this operation, even when running in isolation without any other database workload. To put this in perspective, 6,000 IOPS for the read is virtually identical to what our production database handles under normal operating conditions.&lt;/li&gt;
&lt;li&gt;We can get 6-8x performance based on the same query that we had when we switched to partitioned tables.&lt;/li&gt;
&lt;li&gt;Prewarming the partitioned table only took 3 hours compared to the original table which was 10 hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once we have the right statistics, we made the necessary arrangements such as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Announcing the day and time where we will do this on the production database.&lt;/li&gt;
&lt;li&gt;Increase the storage capacity to ensure our database can fit the new table and still have extra spaces left until we drop the original table. We also need to consider the amount of storage needed for everyday tasks.&lt;/li&gt;
&lt;li&gt;Increase the IOPS so that the Primary and the Replicas can handle the load due to the data copy process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Go-Live Day
&lt;/h3&gt;

&lt;p&gt;The work will start at the beginning of the week to minimize weekend work and ensure our engineers have a peaceful weekend. We also have a backup engineer and SRE support.&lt;/p&gt;

&lt;p&gt;It’s quite normal that things didn’t go as planned, but the first challenge was something that I didn’t expect at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge #1: Our original table was so bad that I couldn’t even complete copying one day's worth of data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Based on what we did during the dry run, we should be able to copy one day of data within 2-3 minutes. However, when it comes to production, I didn’t expect it was going to be much worse. Technically, the production data should have sufficient cache as it is actively being used. I didn’t spend too much time looking into it, but I know I can’t warm up the table as we won’t have enough IOPS for it.&lt;/p&gt;

&lt;p&gt;We have 8 years of data that we need to copy. So, imagine waiting 15 minutes for 1 day of data. Actually, I don’t know how long it would take as I just kill the query after 15 minutes.&lt;/p&gt;

&lt;p&gt;What was the solution? Well, we know for a fact from our dry run, if we warm up the table, it will only take 2-3 minutes for one day of data. But, we cannot warm up our production’s table. So, what can we do?&lt;/p&gt;

&lt;p&gt;I remember a Postgres feature called &lt;a href="https://www.postgresql.org/docs/current/ddl-foreign-data.html" rel="noopener noreferrer"&gt;Foreign data wrappers&lt;/a&gt;. Basically, we will read from another host and write to the partitioned table in the production’s host. This way, we don’t have to warm up the table in the production and we also won’t use too much IOPS as well. This seems like a win to us.&lt;/p&gt;

&lt;p&gt;Based on that idea, we improvised our plan for a little bit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provision another production grade database.&lt;/li&gt;
&lt;li&gt;Prewarm the table.&lt;/li&gt;
&lt;li&gt;Setup Foreign data wrapper.&lt;/li&gt;
&lt;li&gt;Update our copy script to read from the new host and write to the current production’s database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This whole process set us back by two to three days. But, it’s something that we cannot avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge #2: Warming up all databases, including replicas, was necessary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We didn't realize we needed to warm up our replicas until the go-live day began. We had only been focusing on the primary database. This oversight added extra work to the process but at least we are not so worried about the partitions not warmed up enough in the replicas.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The final query
&lt;/h4&gt;

&lt;p&gt;Once we have gone through all of the tasks, we just flip the switch by renaming the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Remove existing trigger&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;

&lt;span class="c1"&gt;-- THE IMPORTANT BITS&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;prices_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;prices_partitioned&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create the trigger function&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;sync_prices_changes_v2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- ...&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Attach the trigger on the new table so that prices_old will get the changes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;sync_to_partitioned_table&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;sync_prices_changes_v2&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we are using triggers to copy the data from and to the new and the old table. We still need the old table. Remember, things could go wrong and we need our Plan B, C and so on.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Did Right
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table warm-ups:&lt;/strong&gt; Based on past experience from the database upgrade, we made the right call to warm up the partitions. This ensured that query time didn't increase when we switched from the unpartitioned table to the partitioned table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripted the tasks:&lt;/strong&gt; We prepared scripts for every task and developed a small Go app to manage data copying. The app included essential features like timestamps, the ability to specify the year for data copying, and the time taken to copy data. We also created an app for warming up the table, allowing us to carefully manage CPU and IOPS usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used CloudWatch Dashboard:&lt;/strong&gt; We decided to fully utilize CloudWatch Dashboard for this project, and it proved invaluable for monitoring IOPS, CPU, Replica Lag, and other metrics across multiple replicas. Learning to set up the vertical line feature was particularly helpful for visualizing before-and-after comparisons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team Backup:&lt;/strong&gt; Having backup from another person or team was beneficial. They helped identify things we might have missed and provided a sounding board for ideas during planning and execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Now let’s go to the fun part. However, there was also a regression after we switched to the partitioned table. Details are in the “What we would do differently” section.&lt;/p&gt;

&lt;p&gt;When we are talking about the result, we should return back to why we are doing this in the first place. On the micro level, we want to reduce the IOPS for certain queries. On the macro level, we want our endpoints to be faster and more resilient towards requests spikes. Severe high IOPS can cause replica lags as well.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  IOPS
&lt;/h3&gt;

&lt;p&gt;The IOPS was reduced by 20% right after this exercise. Since this table was being used extensively across all of our applications, we can reduce the maximum IOPS thus allowing us to save our costs further. To be clear, we are running multiple replicas so the cost savings are multiplied by the number of replicas that we have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F293xfzj2gueosw2lhnzb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F293xfzj2gueosw2lhnzb.png" alt="IOPS" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Response Time
&lt;/h3&gt;

&lt;p&gt;This was quite significant as we managed to reduce the p99 from 4.13s to 578ms. That is a about 86% reduction in terms of the response time. You can see how flat the chart is right after we made the switch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz7z3wez7v5upmnyatle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz7z3wez7v5upmnyatle.png" alt="Response Time" width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Replica Lag
&lt;/h3&gt;

&lt;p&gt;Right before we made the table switch, we had increased usage of the affected endpoints causing higher IOPS which in result caused us replica lags. But, it went away the moment we flipped to the partitioned table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fandcu2ufh770yo4qx3gd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fandcu2ufh770yo4qx3gd.png" alt="Replica Lag" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Block the deployments
&lt;/h3&gt;

&lt;p&gt;The biggest mistake in our planning was not blocking deployments on the day of the switch. While the switch itself wouldn't disrupt others' work, we overlooked the fact that two deployments were related to the table we were optimizing. This caused confusion about the impact of the table partitioning.&lt;/p&gt;

&lt;p&gt;The higher CPU and IOPS utilization observed after the switch put the exercise at risk of rollback. We eventually identified that an earlier deployment caused the problem. However, pinpointing the cause required rolling back the changes and extensive discussion. This situation could have been avoided by blocking deployments for a day to clearly assess the impact of our changes.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Too focused on the replicas
&lt;/h3&gt;

&lt;p&gt;Our focus on resolving the replica issue led us to overlook the impact on the primary database. We failed to identify which queries would be affected, and one query, in particular, performed worse after the switch. This query triggered scans across all of the table partitions, increasing IOPS and CPU usage. By modifying the query, we managed to resolve it.&lt;/p&gt;

&lt;p&gt;This experience highlighted that without the correct query, table partitioning can be detrimental. In this instance, the query lacked a lower limit for the date range, resulting in all partitions being scanned unnecessarily. Interestingly, the same query performed well on the unpartitioned table.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Extra partitions
&lt;/h3&gt;

&lt;p&gt;This was another instance where we noticed a regression on one endpoint that helped us realize the mistake.&lt;/p&gt;

&lt;p&gt;To reduce the future workload of creating partitions, we initially created all the partitions for 2025. We discovered that one query was structured like  'created_at &amp;gt; ?' without an upper limit. It caused the query to scan future partitions that were empty. By removing these partitions, we fixed the issue.&lt;/p&gt;

&lt;p&gt;Going forward, we need to determine a better strategy for when to create future partitions.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Incremental release
&lt;/h3&gt;

&lt;p&gt;We shouldn’t have waited for the entire table to be migrated before starting to use it. Since most users only access data from the last four partitions (or months), we could have implemented a feature toggle to direct queries for data after a specific date to the already migrated partitioned tables.&lt;/p&gt;

&lt;p&gt;This approach would have allowed us to start using the partitioned tables sooner and reduced the risks and potential negative impact of any issues that might arise. As it stands, rolling back our changes would be costly in terms of IOPS, as we would need to prewarm the old table again to avoid production downtime due to cold cache.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This is just the beginning. With this experience under our belt, we can start exploring the possibility of implementing table partitions on other tables as well. Of course, table partitioning isn't a one-size-fits-all solution. We need to diagnose the issue before proceeding. Sometimes, something as simple as adding an index can resolve the problem.&lt;/p&gt;

&lt;p&gt;In conclusion, while partitioning the 1TB+ "prices" table presented some challenges, especially during the go-live phase, the overall outcome was substantial performance improvements. This initiative aligns with the API Team's ongoing goal: providing the best possible experience to better serve our customers. Our API is now more stable and resilient against sudden spikes in requests during peak hours.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>rails</category>
    </item>
    <item>
      <title>Error Budgets in Practice: A Data-Driven Approach to Risk and Release Management</title>
      <dc:creator>Hakim Zulkhibri</dc:creator>
      <pubDate>Mon, 20 Jan 2025 11:29:29 +0000</pubDate>
      <link>https://dev.to/coingecko/error-budgets-in-practice-a-data-driven-approach-to-risk-and-release-management-2m9b</link>
      <guid>https://dev.to/coingecko/error-budgets-in-practice-a-data-driven-approach-to-risk-and-release-management-2m9b</guid>
      <description>&lt;h2&gt;
  
  
  Why Error Budgets?
&lt;/h2&gt;

&lt;p&gt;CoinGecko offers API services to our customers. There are 2 types of APIs that we provide, Public API and &lt;a href="https://www.coingecko.com/en/api?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=eng-content&amp;amp;utm_content=error-budgets" rel="noopener noreferrer"&gt;Pro API&lt;/a&gt;. For Pro API, we are bound with tight &lt;strong&gt;service-level agreements (SLA)&lt;/strong&gt; to our customers. These SLAs are important for us to ensure customer satisfaction and trust in the platform.&lt;/p&gt;

&lt;p&gt;We visualized the risk metrics to categorize risk categories into severities that may impose danger to our SLAs. Instead of settling for availability goals like 99.9% or 99.95%, we strive for tangible information to determine to ensure that our goals remain realistic.&lt;/p&gt;

&lt;p&gt;In this article, we will discuss the process behind measuring and managing a reliable &lt;strong&gt;uptime&lt;/strong&gt; SLA. How do we track, analyse and understand risks before reaching a conclusion for our SLA?&lt;/p&gt;

&lt;p&gt;For the ease of understanding, let’s first talk about SLOs and SLAs:-&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLA&lt;/strong&gt;&lt;br&gt;
Service Level Agreements – agreements with our customers about reliability of our services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLO&lt;/strong&gt;&lt;br&gt;
Service Level Objectives – thresholds that catch an issue before it breaches our SLAs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i31opdsosv1lzi96uyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6i31opdsosv1lzi96uyu.png" alt="SLA and SLO, where it stands - Courtesy of Google" width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;SLA and SLO, where it stands - Courtesy of Google&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In other words, we have a higher threshold for SLO compared to SLA. We need to capture any issues before it reaches out to the customer. In terms of uptime, internally, we only allow a lower duration of downtime, x, compared to the external threshold that we set, y duration of downtime. To put it into formula → x &amp;lt; y.&lt;/p&gt;

&lt;p&gt;The SLA that we have for our Pro API is 99.9%. That means, for SLO, we have a higher threshold; e.g., 99.95% or 99.99%.&lt;/p&gt;
&lt;h3&gt;
  
  
  How do we know how much head room that we have before we breach our SLO?
&lt;/h3&gt;

&lt;p&gt;An uptime SLA of 99.9% is equivalent to 43.2 minutes of downtime in a month. A corresponding SLO of 99.95% is equivalent to 21.6 minutes of downtime.&lt;/p&gt;

&lt;p&gt;This difference in minutes is also known as Error Budget. This error budget allows us to do maintenance, deployment and improvement towards our application. Engineers only have 21.6 minutes a month to maneuver around when they face problems that cause downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Budget&lt;/strong&gt; is an inverse of SLO. If our SLO is 99.9% availability, our Error Budget is the remaining amount of time (0.1% unavailability).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh3gxkyusumpp0ho04x1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh3gxkyusumpp0ho04x1.png" alt="Availability Table - Courtesy of Google" width="800" height="723"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Availability Table - Courtesy of Google&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Calculating unavailability formula in terms of minutes per month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Desired SLO
slo = 99.9

// Days in a month used in the formula
month = 30

// Total minutes in a month; 30 days * 24 hours * 60 minutes
mins_per_month = 43200 

// Calculate allowed downtime in minutes per month
unavailability_mins = (100 - slo) / 100 * mins_per_month
unavailability_mins = 0.1 / 100 * 43200
unavailability_mins = 43.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the table above, we now understand the unavailability, i.e., the Error Budget, in terms of the time we can afford.&lt;/p&gt;

&lt;p&gt;Let’s take a look at these 2 diagrams below to understand the burn rate of our Error Budget from Day 1 to Day 28. The diagram below shows our Error budget of 21.6 mins (expressed as 100%) at the beginning of the month.&lt;/p&gt;

&lt;p&gt;The first diagram shows a positive Error Budget by the 28th day of the month. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1gbp7ewsgn1ukrx0j3n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1gbp7ewsgn1ukrx0j3n.png" alt="Monthly error budget nearing the budget - Courtesy of Google" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Monthly error budget nearing the budget - Courtesy of Google&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Meanwhile, the following diagram shows a breached Error Budget with negative percentage remaining.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feucsyunz63q98lyk27sa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feucsyunz63q98lyk27sa.png" alt="Monthly error budget breaching the budget - Courtesy of Google" width="800" height="493"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Monthly error budget breaching the budget - Courtesy of Google&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagram above provides a visual representation of the burn rate in percentage regardless of how many minutes of Error Budget that we have chosen.&lt;/p&gt;

&lt;p&gt;Error budget burn rate can be monitored throughout the month to revise the frequency, priority and type of deployments scheduled.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing past incidents to categorize our failure points
&lt;/h2&gt;

&lt;p&gt;No application or system is perfect, especially in its early stages. The key is to learn from these experiences by recording, documenting, and categorizing each incident for future reference. By investigating these issues, we gain a deeper understanding of how to prioritize and address them, helping us craft realistic SLOs. Analyzing past incidents and anticipating future ones allows us to take proactive measures to prevent SLA breaches and ensure system reliability.&lt;/p&gt;

&lt;p&gt;First thing first is we have to understand our failure points. Categorize each incident that occurred or may occur in the future. This is what we call a &lt;strong&gt;risk&lt;/strong&gt;. This helps us to create a high-level view to view which categories cause us the most headache.&lt;/p&gt;

&lt;p&gt;From where do we obtain the information of risks? Historical data, industry best practices, brainstorming etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For example purposes&lt;/strong&gt;, these are some of the categories that we identified that can cause downtime to our application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disaster recovery drill&lt;/li&gt;
&lt;li&gt;Updating major code version&lt;/li&gt;
&lt;li&gt;Code deployment misconfiguration&lt;/li&gt;
&lt;li&gt;Unoptimized database queries&lt;/li&gt;
&lt;li&gt;Software defects in the code&lt;/li&gt;
&lt;li&gt;Breakdown of caching service&lt;/li&gt;
&lt;li&gt;Outage in an Availability Zone&lt;/li&gt;
&lt;li&gt;Unintended data loss or corruption &lt;/li&gt;
&lt;li&gt;Malicious security breach/attack&lt;/li&gt;
&lt;li&gt;High volume of traffic&lt;/li&gt;
&lt;li&gt;Breakdown in the message queue system&lt;/li&gt;
&lt;li&gt;Disk failure&lt;/li&gt;
&lt;li&gt;Third-party dependency failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, from each of the incidents, we calculate:&lt;br&gt;
&lt;strong&gt;ETTD - Estimated Time To Detection&lt;/strong&gt; – how long it would take to detect and notify a human (or robot) that the incident has occurred; aka MTTD (Mean Time to Detect).&lt;br&gt;
&lt;strong&gt;ETTR - Estimated Time To Resolution&lt;/strong&gt; – how long it would take to fix the incident once the human (or robot) has been notified; aka MTTR (Mean Time to Repair).&lt;br&gt;
&lt;strong&gt;ETTF - Estimated Time To Failure&lt;/strong&gt; – estimated frequency between instances of this incident; aka MTBF (Mean Time Between Failure). &lt;br&gt;
&lt;strong&gt;% of Users Affected&lt;/strong&gt; – Percentage of users was affected by the failure&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffooymih0qg8ytgyctvk9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffooymih0qg8ytgyctvk9.png" alt="Above terms visualized - Courtesy of Google" width="800" height="334"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Above terms visualized - Courtesy of Google&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This helps us understand the frequency, time, and our swiftness in responding towards an incident.&lt;/p&gt;

&lt;p&gt;We wanted to understand how much downtime (bad minutes) per year is caused by a single category. From this valuable information, we open up a spreadsheet, fill in all of our data, and calculate our risk level for each category. This is what we call the &lt;strong&gt;Risk Catalog&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facn09jkrtsl571jjwybh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facn09jkrtsl571jjwybh.png" alt="Example of a Risk Catalog" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We enter our list of risks in the blue cells together with the ETTD, ETTR, Percentage of impact towards users and ETTF. Based on our inputs, we are able to see the number of incidents per year and bad minutes per year generated by the spreadsheet formula in the grey cells.&lt;/p&gt;

&lt;h2&gt;
  
  
  Computed Stack Rank of Risks
&lt;/h2&gt;

&lt;p&gt;We took the information above and rearranged the risks based on a severity level to a new spreadsheet called the &lt;strong&gt;Risk Stack Rank&lt;/strong&gt;. This is how we can calculate and provide a data-driven context on how we stand today vs our current SLO defined.&lt;/p&gt;

&lt;p&gt;Let’s have a look at the computed stack rank of risks below:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqklplzi504p3zch02tdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqklplzi504p3zch02tdw.png" alt="Computed stack rank of risks" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the sheet above, we can see that our risks are populated and arranged by bad minutes per year. The most bad minutes per year will be considered as the highest risk&lt;/p&gt;

&lt;p&gt;Risk Stack Rank has multiple components to look for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target Availability&lt;/strong&gt;&lt;br&gt;
The desired availability in percentage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget (m/yr)&lt;/strong&gt;&lt;br&gt;
The total error budget available, measured in minutes per year (m/yr), which represents the maximum allowable downtime while still meeting the target availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accepted (m/yr)&lt;/strong&gt;&lt;br&gt;
The amount of downtime already allocated for various known risks in minutes per year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unallocated Budget (m/yr)&lt;/strong&gt;&lt;br&gt;
The portion of the error budget that remains uncommitted after accounting for known and accepted risks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threshold of unacceptability for an individual risk (% of error budget)&lt;/strong&gt;&lt;br&gt;
A limit that defines how much of the total error budget a single risk can consume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too Big Threshold (m/yr) – for a single risk&lt;/strong&gt;&lt;br&gt;
The absolute upper limit for the amount of downtime a single risk can be responsible for. If the expected impact of a risk exceeds this threshold, the risk is deemed "too big" and must be mitigated, as it could jeopardize the ability to meet the SLO.&lt;/p&gt;

&lt;p&gt;In terms of the colored cell, below are the explanation of it:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frceeoxjq9a8awdwb8c50.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frceeoxjq9a8awdwb8c50.png" alt="Cell colors definition" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Red – this risk is &lt;strong&gt;unacceptable&lt;/strong&gt;, as it falls above the acceptable error budget for a single risk.&lt;br&gt;
Amber – this risk &lt;strong&gt;should not be acceptable&lt;/strong&gt;, as it’s a major consumer of our error budget and therefore, needs to be addressed.&lt;br&gt;
Green – this is an &lt;strong&gt;acceptable&lt;/strong&gt; risk. It's not a major consumer of our error budget, and in aggregate, does not cause our application to exceed the error budget.&lt;br&gt;
Blue – this risk &lt;strong&gt;has been accepted&lt;/strong&gt; to fit within our error budget. Accepting a risk means planning not to fix it and taking the outage and corresponding hit on the error budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Risk Stack Rank in Practice
&lt;/h2&gt;

&lt;p&gt;Remember the risks that we have entered in the Risk Catalog together with its metrics? This Risk Stack Rank calculates the risks and rank it according to bad mins/year.&lt;/p&gt;

&lt;p&gt;In this subsection, assume that &lt;strong&gt;we want to have a 3-nines availability target (99.9%)&lt;/strong&gt;, we have 2 red-shaded (unacceptable) risks and the others are green-shaded (acceptable) risks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flp9v1uf19vcs2qf9shva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flp9v1uf19vcs2qf9shva.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see some scenarios below to see it in action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accepting a Risk that is in Red or Amber-shaded
&lt;/h3&gt;

&lt;p&gt;Say that our threshold of unacceptability for an individual risk is 25% of the error budget. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshgvcfzxvqhklvm5ibv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshgvcfzxvqhklvm5ibv0.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see from above that accepting “Third-party dependencies failure” causes green-shaded risks to turn into amber-shaded risks (should not be accepted). This happens due to the accepted risks already consuming a number of error budgets, causing other risks to impose danger to our error budget.&lt;/p&gt;

&lt;p&gt;Say we accept more of the risks that will consume our error budget.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9gvi7r0bsz2prrdqtzcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9gvi7r0bsz2prrdqtzcx.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram above shows that more risks are amber-shaded. This means that we have to act upon the risks to bring down the bad mins/year. We’ll discuss this in Improving our Risk Stack Rank section. &lt;/p&gt;

&lt;h3&gt;
  
  
  Ideal Situation
&lt;/h3&gt;

&lt;p&gt;We can start by accepting the green risks and see how it consumes our error budget in this sheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frc7epx1mr10d8nk9s9u2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frc7epx1mr10d8nk9s9u2.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the figure above we can see that we have accepted 519.62 out of 525.96 minutes from our error budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unaccepted Risks
&lt;/h3&gt;

&lt;p&gt;As we accepted risks (marked y), we agreed to accept it without any mitigation actions that are required. These risks are now known as risks that will burn our error budget.&lt;/p&gt;

&lt;p&gt;But how about unaccepted risks that are in the red or amber-shaded? What do we do with them?&lt;/p&gt;

&lt;p&gt;If we do accept them, the sheet will show that we have breached our error budget. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh54zn3k4fzbmnu4f6ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh54zn3k4fzbmnu4f6ui.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that our &lt;strong&gt;Unallocated Budget&lt;/strong&gt; section has reached a negative value.&lt;/p&gt;

&lt;p&gt;These risks (red and amber-shaded) are the risks that &lt;strong&gt;require mitigation actions&lt;/strong&gt;, these risks have to be acted upon so that it does not impose danger to our Error Budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to implement Error Budgets in practice
&lt;/h2&gt;

&lt;p&gt;Now that we have Service Level Objective (SLO) and Error Budget, let's enforce it! It is important that everyone in the organization is aware of this policy, especially the engineering and product team. &lt;/p&gt;

&lt;p&gt;This sets as a baseline in determining whether we can release new features or deploy a hotfix in case the Error Budget is nearing its limit or has been breached.&lt;/p&gt;

&lt;p&gt;To simplify things in this article, we are going to present 3 tiers of severity levels – Tier 1, Tier 2 and Tier 3 – and with Call to Action (CTA) in each tier.&lt;/p&gt;

&lt;p&gt;How do we know which is which? Again, quantifying this is crucial in understanding the criticality of an issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1&lt;/strong&gt;&lt;br&gt;
Description: There is a depletion of Error Budget within X days (e.g. 14 days) and the Error Budget percentage is still within acceptable status.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;CTA: Acknowledgement is required and the SRE team will notify the application team.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2&lt;/strong&gt;&lt;br&gt;
Description: The Error Budget has depleted until Y% (e.g. 50%) within 28 days and the Error Budget percentage is in warning status.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;CTA: Halt releases and P0 issues or security fixes until SLO recovers; setup dedicated team to investigate AND SRE team to highlight to application team.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3&lt;/strong&gt;&lt;br&gt;
Description: There is a major depletion of Error Budget within X days (e.g. 2 days) OR the Error Budget has reached Z% (e.g. 80%) or lower.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;CTA: All-hands-on-deck to focus on resolving the service outage. Inform top management. Use a "silver bullet" (see next section on Silver Bullets) carefully upon multiple approvals at this stage. Prepare a PR statement if needed.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By categorizing incidents into these tiers, the team can respond proportionally to the severity of an issue. This ensures resources are allocated effectively while maintaining adherence to SLOs. &lt;/p&gt;

&lt;h2&gt;
  
  
  SLO Recovery
&lt;/h2&gt;

&lt;p&gt;When SLOs reach warning levels, two immediate actions are crucial – first, a dedicated task force comprising application developers and SRE must be assembled to address the situation. Second, all ongoing releases must be temporarily suspended during the investigation phase.&lt;/p&gt;

&lt;p&gt;An improved release planning strategy is fundamental to SLO improvement, particularly in environments practicing Continuous Deployment. The foundation of this strategy involves categorizing deployments based on their risk levels.&lt;/p&gt;

&lt;p&gt;The classification of deployments into risk categories requires careful consideration of various factors. While the specific criteria for risk assessment may vary by organization, they typically consider factors such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scope of changes&lt;/li&gt;
&lt;li&gt;Potential impact on critical user paths&lt;/li&gt;
&lt;li&gt;Architectural modifications&lt;/li&gt;
&lt;li&gt;Integration points with external systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For deployments classified as high-risk, several key practices should be implemented. The implementation of a daily stagger system ensures that high-risk deployments are spread across different days, allowing for precise identification and swift rollback of problematic changes if necessary. All high-risk deployments must be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documented in a shared engineering calendar&lt;/li&gt;
&lt;li&gt;Clearly communicated to all team members&lt;/li&gt;
&lt;li&gt;Monitored by relevant stakeholders during and after deployment&lt;/li&gt;
&lt;li&gt;Scheduled with consideration for key personnel availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An often overlooked but crucial aspect of release management is ensuring the availability of critical stakeholders during deployment windows. This includes considering team members' leave schedules when planning significant releases.&lt;/p&gt;

&lt;p&gt;By implementing these controls, teams can effectively shield their remaining error budget from new incidents, allowing their SLOs to gradually recover as the measurement window advances and maintaining system stability during the recovery period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We assess risks by understanding how much downtime or error the system can tolerate while still meeting SLOs. We use error budgets to track acceptable failure, balance reliability with deployments, and prioritize risks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rather than stating without evidence that we aim for 99.9%, 99.95%, or any other availability goal, we now have concrete data that shows whether or not our goal is feasible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By analyzing historical data, potential failures, and user impact, we can determine if an Error Budget is realistic and adjust accordingly to ensure system stability without holding back progress. This approach ensures that every decision—whether to deploy a new feature or address a critical issue—is backed by measurable insights and aligned with the organization’s goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sachto, A. (2022, May 5). How SREs analyze risks to evaluate SLOs. Google Cloud Blog. &lt;a href="https://cloud.google.com/blog/products/devops-sre/how-sres-analyze-risks-to-evaluate-slos" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/products/devops-sre/how-sres-analyze-risks-to-evaluate-slos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Brown, M. (2017, May 23). How to prioritize and communicate risks—CRE life lessons. Google Cloud Blog. &lt;a href="https://cloud.google.com/blog/products/gcp/know-thy-enemy-how-to-prioritize-and-communicate-risks-cre-life-lessons" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/products/gcp/know-thy-enemy-how-to-prioritize-and-communicate-risks-cre-life-lessons&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Training, G. C. (n.d.). SLOs vs SLAs [Video]. Coursera. &lt;a href="https://www.coursera.org/learn/site-reliability-engineering-slos/lecture/KpI1q/slos-vs-slas" rel="noopener noreferrer"&gt;https://www.coursera.org/learn/site-reliability-engineering-slos/lecture/KpI1q/slos-vs-slas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Training, G. C. (n.d.-a). Error budgets [Video]. Coursera. &lt;a href="https://www.coursera.org/learn/site-reliability-engineering-slos/lecture/N12XI/error-budgets" rel="noopener noreferrer"&gt;https://www.coursera.org/learn/site-reliability-engineering-slos/lecture/N12XI/error-budgets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Warner, A., Bramley, A., &amp;amp; Hilton, A. (2018, June 28). SRE at Google: Good housekeeping for error budgets. Google Cloud Blog. &lt;a href="https://cloud.google.com/blog/products/devops-sre/good-housekeeping-error-budgetscre-life-lessons" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/products/devops-sre/good-housekeeping-error-budgetscre-life-lessons&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sre</category>
      <category>devops</category>
      <category>release</category>
      <category>sla</category>
    </item>
    <item>
      <title>How to create test cases using Equivalence Partitioning and Boundary Value Analysis test technique (Step by step)</title>
      <dc:creator>Alan Liew</dc:creator>
      <pubDate>Mon, 15 Jul 2024 06:13:30 +0000</pubDate>
      <link>https://dev.to/coingecko/how-to-create-test-cases-using-equivalence-partitioning-and-boundary-value-analysis-test-technique-step-by-step-1nkk</link>
      <guid>https://dev.to/coingecko/how-to-create-test-cases-using-equivalence-partitioning-and-boundary-value-analysis-test-technique-step-by-step-1nkk</guid>
      <description>&lt;p&gt;In the world of software testing, ensuring comprehensive coverage of test cases while maintaining efficiency is a critical challenge. Among the range of techniques available, Boundary Value Analysis (BVA) and Equivalence Partitioning (EP) stand out as fundamental methods that &lt;strong&gt;significantly enhance the effectiveness of test design.&lt;/strong&gt; These techniques help identify critical test cases and ensure that the testing process is both systematic and thorough. &lt;/p&gt;

&lt;p&gt;In this blog post, we will delve into the principles of BVA and EP, exploring how they can be applied to optimize testing efforts and improve software quality.&lt;/p&gt;

&lt;p&gt;Both are black box testing techniques. Let’s start with an explanation of the Equivalence Partitioning technique.&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Equivalence Partitioning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The steps should be as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Divide the test conditions into groups or sets of partitions. All the elements under the same set of partitions are considered the same, and the system should handle them equivalently.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;A partition containing valid values is called a valid partition&lt;/li&gt;
&lt;li&gt;A partition containing invalid values is called an invalid partition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For non-numeric example:&lt;/strong&gt;&lt;br&gt;
If the fruits = apple, then print green&lt;br&gt;
If the fruits = orange, then print the orange&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqloek8dv0vkoxft1jzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqloek8dv0vkoxft1jzy.png" alt="Non Numeric Example" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the above example, Apple and Orange are &lt;em&gt;valid partitions&lt;/em&gt;, while no invalid partition is specified; we assumed other fruits are &lt;em&gt;invalid partitions&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For numeric example :&lt;/strong&gt;&lt;br&gt;
Users should be able to register when they are between ages 1 and 21.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs507j4wbk43r9pr3itvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs507j4wbk43r9pr3itvs.png" alt="1 and 21 is a valid partition(able to register), while an age less than 1 and more than 21 is an invalid partition" width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the above example, an age between &lt;em&gt;1 and 21 is a valid partition(able to register)&lt;/em&gt;, while an age between &lt;em&gt;less than 1 and more than 21 is an invalid partition(unable to register)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. When there are multiple sets of partitions or more than one input,&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The valid partition shall combined.&lt;/li&gt;
&lt;li&gt;The invalid partition should be tested individually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Let’s take this as an example; the requirement is to register a user email when it hits the requirements below:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It should be 6–30 characters long&lt;/li&gt;
&lt;li&gt;Should be alphanumeric&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqks0cbjkob1m36syn100.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqks0cbjkob1m36syn100.png" alt="We should be able to come up with these valid and invalid partitions." width="254" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Following the No. 2 rules, we don't need to &lt;em&gt;&lt;strong&gt;create 5 test cases for 3 partitions belonging to (6–30 characters long)&lt;/strong&gt;&lt;/em&gt; and &lt;em&gt;&lt;strong&gt;2 partitions belonging to (should be alphanumeric)&lt;/strong&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We shall combine the valid partition and come out with the same test value (input) of abc123.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fih2ii7gglc965byc1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fih2ii7gglc965byc1o.png" alt="We can satisfy the valid partition with the value of “abc123”" width="257" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, to test the invalid partition individually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdv495yzdn0330cwdhlcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdv495yzdn0330cwdhlcn.png" alt="4 test cases will be needed to cover all partitions." width="256" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, 4 test cases are sufficient to test out all the combinations of partitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question may arise: why don't we combine the invalid partition?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, if we test only with less than 6 characters, the error message is “Sorry, your username must be between 6 and 30 characters long”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpbuj0r1itvfkme83udo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdpbuj0r1itvfkme83udo.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, when we tested the &lt;strong&gt;invalid partition of &amp;lt;6 characters&lt;/strong&gt; and the &lt;strong&gt;invalid partition of not alphanumeric&lt;/strong&gt;, the error message was “Sorry, only letters (a-z), numbers (0–9), and periods (.) are allowed.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus36y3keas23nkqsz6as.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fus36y3keas23nkqsz6as.png" alt=" " width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s clear that when we combine two invalid partitions to test, it’s very easy to miss out on validating the invalid partition of &amp;lt;6 characters. In this case, the test cases are not well designed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;﻿✅ Boundary Value Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Boundary value analysis is closely related to equivalence partitioning. We can determine the boundary value by knowing the partition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We will use back the same numeric example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users should be able to register when they are between ages 1 and 21&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbbojno1jamnstfka64vn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbbojno1jamnstfka64vn.png" alt=" " width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And we know that&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;age between 1 and 21 is a valid partition (able to register),&lt;/li&gt;
&lt;li&gt;age less than 1 and more than 21 is an invalid partition (unable to register).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To determine what is the boundary values:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: The BVA is that the minimum and maximum values of the partition are called the partition's boundary values.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hence, the boundary values of (1 to 21) are &lt;strong&gt;1 and 21&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next, let's find out what the 2-value BVA or 3-value BVA are.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For 2-Value BVA: This boundary value and its closest neighbor belonging to the adjacent partition will be boundary values&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8oaflilchjbo77tgvazl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8oaflilchjbo77tgvazl.png" alt=" " width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boundary values of (1 to 21) are the values of &lt;strong&gt;1 and 21.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(Adjacent partition) Boundary values of (1 to 21) are the values of &lt;strong&gt;0 and 22.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hence, the 2-value BVA are the values of &lt;strong&gt;0, 1, 21, 22&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;For 3-Value BVA: This boundary values and both their neighbors will be boundary values.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Boundary values of (1 to 21) are the values of &lt;strong&gt;1 and 21.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(adjacent partition) Boundary values of (1 to 21) are the values of &lt;strong&gt;0 and 22&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5f4p7m2ky3jf0zf95jmv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5f4p7m2ky3jf0zf95jmv.png" alt=" " width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boundary values and both its neighbors of (1 to 21) are the values of &lt;strong&gt;0, 1, 2, 20, 21, 22.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvxbjy4zd9v42dk8q438.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvxbjy4zd9v42dk8q438.png" alt=" " width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boundary values and both its neighbors of (0 and 22) are the values of &lt;strong&gt;1, 0, 1, 21, 22, 23.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hence, the 3-value BVA are the &lt;strong&gt;values of -1, 0, 1, 2, 20, 21, 22, 23&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Now, you may wonder when we should use 2-value BVA or 3-value BVA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let’s try another requirement:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users should be able to register when their age less or equal to 21&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And somehow, developers make this mistake in the logic to be able to register when &lt;strong&gt;age = 21.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We may have overlooked and missed this bug because the 2-value BVA only tests 21; however, &lt;strong&gt;a 3-value BVA may be able to surface the issue because the value of 20 is tested.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Boundary Value Analysis and Equivalence Partitioning are useful techniques for any software tester. By focusing on critical edge cases and grouping inputs into meaningful categories, these techniques &lt;strong&gt;avoid exhaustive testing, streamline the testing process, and uncover potential defects&lt;/strong&gt; that might otherwise go unnoticed. Embracing these methods will enhance your testing strategy and ensure that your applications meet the highest standards of quality and performance.&lt;/p&gt;

</description>
      <category>softwaretesting</category>
      <category>qa</category>
    </item>
  </channel>
</rss>
