Optimizing SQL Server Performance With AI: Automating Query Optimization and Predictive Maintenance
Mastering Retrieval Augmented Generation: From Fundamentals to Advanced Techniques
Observability and Performance
The dawn of observability across the software ecosystem has fully disrupted standard performance monitoring and management. Enhancing these approaches with sophisticated, data-driven, and automated insights allows your organization to better identify anomalies and incidents across applications and wider systems. While monitoring and standard performance practices are still necessary, they now serve to complement organizations' comprehensive observability strategies. This year's Observability and Performance Trend Report moves beyond metrics, logs, and traces — we dive into essential topics around full-stack observability, like security considerations, AIOps, the future of hybrid and cloud-native observability, and much more.
Java Application Containerization and Deployment
Software Supply Chain Security
Metaprogramming is a powerful programming paradigm that allows code to dynamically manipulate its behavior at runtime. JavaScript, with the introduction of Proxies and the Reflect API in ES6, has taken metaprogramming capabilities to a new level, enabling developers to intercept and redefine core object operations like property access, assignment, and function invocation. This blog post dives deep into these advanced JavaScript features, explaining their syntax, use cases, and how they work together to empower dynamic programming. What Are Proxies? A Proxy in JavaScript is a wrapper that allows developers to intercept and customize fundamental operations performed on an object. These operations include getting and setting properties, function calls, property deletions, and more. Proxy Syntax JavaScript const proxy = new Proxy(target, handler); target: The object being proxied.handler: An object containing methods, known as traps, that define custom behaviors for intercepted operations. Example: Logging Property Access JavaScript const user = { name: 'Alice', age: 30 }; const proxy = new Proxy(user, { get(target, property) { console.log(`Accessing property: ${property}`); return target[property]; } }); console.log(proxy.name); // Logs: Accessing property: name → Output: Alice Key Proxy Traps Trap NameOperation InterceptedgetAccessing a property (obj.prop or obj['prop'])setAssigning a value to a property (obj.prop = value)deletePropertyDeleting a property (delete obj.prop)hasChecking property existence (prop in obj)applyFunction invocation (obj())constructCreating new instances with new (new obj()) Advanced Use Cases With Proxies 1. Input Validation JavaScript const user = { age: 25 }; const proxy = new Proxy(user, { set(target, property, value) { if (property === 'age' && typeof value !== 'number') { throw new Error('Age must be a number!'); } target[property] = value; return true; } }); proxy.age = 30; // Works fine proxy.age = '30'; // Throws Error: Age must be a number! In this example, the set trap ensures type validation before allowing assignments. 2. Reactive Systems (Similar to Vue.js Reactivity) JavaScript const data = { price: 5, quantity: 2 }; let total = 0; const proxy = new Proxy(data, { set(target, property, value) { target[property] = value; total = target.price * target.quantity; console.log(`Total updated: ${total}`); return true; } }); proxy.price = 10; // Logs: Total updated: 20 proxy.quantity = 3; // Logs: Total updated: 30 This code dynamically recalculates values whenever dependent properties are updated, mimicking the behavior of modern reactive frameworks. What Is Reflect? The Reflect API complements Proxies by providing methods that perform default behaviors for object operations, making it easier to integrate them into Proxy traps. Key Reflect Methods MethodDescriptionReflect.get(target, prop)Retrieves the value of a property.Reflect.set(target, prop, val)Sets a property value.Reflect.has(target, prop)Checks property existence (prop in obj).Reflect.deleteProperty(target, prop)Deletes a property.Reflect.apply(func, thisArg, args)Calls a function with a specified this context.Reflect.construct(target, args)Creates a new instance of a constructor. Example: Using Reflect for Default Behavior JavaScript const user = { age: 25 }; const proxy = new Proxy(user, { set(target, property, value) { if (property === 'age' && typeof value !== 'number') { throw new Error('Age must be a number!'); } return Reflect.set(target, property, value); // Default behavior } }); proxy.age = 28; // Sets successfully console.log(user.age); // Output: 28 Using Reflect simplifies the code by maintaining default operations while adding custom logic. Real-World Use Cases Security wrappers: Restrict access to sensitive properties.Logging and debugging: Track object changes.API data validation: Ensure strict rules for API data. Conclusion Metaprogramming with Proxies and Reflect enables developers to dynamically control and modify application behavior. Master these tools to elevate your JavaScript expertise. Happy coding!
Welcome to 2025! A new year is the perfect time to learn new skills or refine existing ones, and for software developers, staying ahead means continuously improving your craft. Software design is not just a cornerstone of creating robust, maintainable, and scalable applications but also vital for your career growth. Mastering software design helps you write code that solves real-world problems effectively, improves collaboration with teammates, and showcases your ability to handle complex systems — a skill highly valued by employers and clients alike. Understanding software design equips you with the tools to: Simplify complexity in your projects, making code easier to understand and maintain.Align your work with business goals, ensuring the success of your projects.Build a reputation as a thoughtful and practical developer prioritizing quality and usability. To help you on your journey, I’ve compiled my top five favorite books on software design. These books will guide you through simplicity, goal-oriented design, clean code, practical testing, and mastering Java. 1. A Philosophy of Software Design This book is my top recommendation for understanding simplicity in code. It dives deep into how to write simple, maintainable software while avoiding unnecessary complexity. It also provides a framework for measuring code complexity with three key aspects: Cognitive Load: How much effort and time are required to understand the code?Change Amplification: How many layers or parts of the system need to be altered to achieve a goal?Unknown Unknowns: What elements of the code or project are unclear or hidden, making changes difficult? The book also discusses the balance between being strategic and tactical in your design decisions. It’s an insightful read that will change the way you think about simplicity and elegance in code. Link: A Philosophy of Software Design 2. Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy Simplicity alone isn’t enough — your code must achieve client or stakeholders' goals. This book helps you bridge the gap between domain experts and your software, ensuring your designs align with business objectives. This is the best place to start if you're new to domain-driven design (DDD). It offers a practical and approachable introduction to DDD concepts, setting the stage for tackling Eric Evans' original work later. Link: Learning Domain-Driven Design 3. Clean Code: A Handbook of Agile Software Craftsmanship Once you’ve mastered simplicity and aligned with client goals, the next step is to ensure your code is clean and readable. This classic book has become a must-read for developers worldwide. From meaningful naming conventions to object-oriented design principles, “Clean Code” provides actionable advice for writing code that’s easy to understand and maintain. Whether new to coding or a seasoned professional, this book will elevate your code quality. Link: Clean Code 4. Effective Software Testing: A Developer’s Guide No software design is complete without testing. Testing should be part of your “definition of done.” This book focuses on writing practical tests that ensure your software meets its goals and maintains high quality. This book covers techniques like test-driven development (TDD) and data-driven testing. It is a comprehensive guide for developers who want to integrate testing seamlessly into their workflows. It’s one of the best software testing resources available today. Link: Effective Software Testing 5. Effective Java (3rd Edition) For Java developers, this book is an essential guide to writing effective and idiomatic Java code. From enums and collections to encapsulation and concurrency, “Effective Java” provides in-depth examples and best practices for crafting elegant and efficient Java programs. Even if you’ve been writing Java for years, you’ll find invaluable insights and tips to refine your skills and adopt modern Java techniques. Link: Effective Java (3rd Edition) Bonus: Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software As a bonus, I highly recommend this book to anyone looking to deepen their understanding of design patterns. In addition to teaching how to use design patterns, this book explains why you need them and how they contribute to building extensible and maintainable software. With its engaging and visually rich style, this book is an excellent resource for developers of any level. It makes complex concepts approachable and practical. Link: Head First Design Patterns These five books and the bonus recommendation provide a roadmap to mastering software design. Whether you’re just starting your journey or looking to deepen your expertise, each offers a unique perspective and practical advice to take your skills to the next level. Happy learning and happy coding! Video
The efficiency and productivity of developers greatly depend on the tools and environments they use. Creating traditional development environments has been a time-consuming process, which results in errors due to inconsistencies across teams and projects. However, with the introduction of Dev Home and Dev Boxes, developers can now have solutions that simplify the setup and management of development environments quickly. This article delves into the concept of Dev Home and Dev Boxes, their advantages, and how they could revolutionize how developers work. Understanding Dev Home and Dev Boxes Dev Home Dev Home is a platform or service that offers developers a customizable development environment. It acts as a hub where developers can easily create, configure, and manage their development setups. Dev Home simplifies the setup process by abstracting the complexities involved in environment configuration tasks, allowing developers to concentrate only on coding rather than handling setup details. Dev Boxes Dev Boxes are containers or virtual machines (VMs) that hold project or development stack environments in an encapsulated manner. Each Dev Box comes pre-loaded with tools, libraries, and dependencies needed for a project or stack. Dev Boxes can be set up and removed as quickly as necessary, giving developers separate space for each project or task. To manage Dev Boxes, you can install the Dev Home extension by going to the Microsoft Store and searching for the Azure Extension for Dev Home from Microsoft Corporation. Key Features and Benefits 1. Consistency and Standardization Dev Home and Dev Boxes encourage consistency and standardization within development teams and projects. By offering set templates and configurations, developers can ensure that everyone is working in a uniform environment, reducing compatibility issues and enhancing teamwork. 2. Environment Setup A major advantage of Dev Home and Dev Boxes is the setup of development environments. Developers no longer have to spend hours configuring their devices or manually installing dependencies. With a few clicks or commands, they can create a Dev Box customized to their needs and immediately start coding. 3. Isolation and Cleanliness Dev Boxes provide isolation and cleanliness by containing development environments in containers or virtual machines. Each Dev Box operates independently from the system, and other Dev Boxes ensure that changes made in one environment do not impact others. This isolation helps prevent conflicts and maintains a state for each project. 4. Scalability and Resource Efficiency Dev Home and Dev Boxes offer scalability and efficient use of resources through containerization or virtualization technologies. Developers have the flexibility to adjust the resources assigned to each Dev Box according to their project needs, allowing for resource utilization and reducing waste. 5. Version Control and Collaboration Dev Home seamlessly integrates with version control systems such as Git, enabling developers to manage their development environments in parallel with their code. This ensures that environment setups can be replicated and monitored, promoting collaboration and facilitating the onboarding of team members. Getting Started With Dev Home and Dev Boxes Setting Up Dev Home Select a Dev Home provider that meets your needs, whether cloud-based platforms like AWS, Azure, or Google Cloud or self-hosted solutions like Docker or Kubernetes.Create a Dev Home environment by configuring the desired development stack, tools, and settings using the provider's interface or command line tools.Once the Dev Home environment is configured, developers can access it through a web-based interface, command line interface, or integrated development environment (IDE) plugins. Utilizing Dev Boxes Design a Dev Box based on a template. Customize it to align with your project requirements by specifying the necessary development stack, tools, and dependencies.Set up Dev Boxes. Utilize the Dev Home interface or command line tools to create Dev Boxes based on the chosen template. These Dev Boxes can be set up as needed or automatically triggered by version control events.Work in Dev Boxes. Developers now have the capability to code, test, and debug applications within their designated Dev Boxes. Each Dev Box provides an independent environment for the project, ensuring uniformity and reproducibility. Here is a screenshot of how to begin selecting an environment provider from the Dev Home app: Key Guidelines for Using Dev Home and Dev Boxes 1. Streamline Environment Setup Simplify the setup of Dev Boxes by using infrastructure as code (IaC) tools such as Terraform, Ansible, or Docker Compose. This guarantees that environment configuration is consistent, repeatable, and under version control. 2. Opt for Containerization or Virtualization Explore containerization tools like Docker or virtualization platforms like Vagrant to encapsulate your Dev Boxes. Containers and virtual machines offer isolation, portability, and scalability features that are advantageous for development environments. 3. Maintain Versioned Environment Configurations Keep track of changes to your Dev Box configurations along with code repositories using Git or another version control system. This enables developers to monitor modifications, revert to states if needed, and collaborate efficiently on environment setups. 4. Uphold Security Best Practices Ensure that both your DevHome setup and the associated DevBoxes comply with security practices such as network segmentation, access controls, encryption protocols, and regular vulnerability assessments. Ensure the protection of data and credentials stored in DevBoxes by implementing security measures to reduce risks. 5. Keep an Eye on Resource Usage Monitor resource consumption and performance metrics of DevBoxes to optimize the allocation of resources and identify any irregularities or performance bottlenecks. Set up monitoring and alert systems to maintain the performance and availability of development environments. Conclusion Dev Home and Dev Boxes bring an approach to how developers create, manage, and interact with development environments. By simplifying environment setup complexities and offering environments, Dev Home and Dev Boxes empower developers to focus on their core task — coding. Offering fast environment setup, scalability, and version-controlled configurations, Dev Home and Dev Boxes provide a solution for software development workflows. By adhering to practices and embracing these cutting-edge tools, organizations can streamline their development procedures, enhance teamwork, and speed up the launch of their products and services.
If you are using the AWS Relational Database Service (RDS) offered managed database services, you may wonder how to strategize database storage size. Strategizing database storage includes understanding the key components of RDS storage, optimizing these storage factors, and capping storage growth by using retention periods. AWS RDS offers managed database services for Oracle, MySQL, PostgreSQL, and SQL Server. These managed services include automated backups, single-click upgrades, replication and high availability, and disaster recovery solutions. Under the hood, all these RDS databases use Amazon Elastic Block Store (EBS) volumes for storage. This post discusses the storage components, optimization steps for these storage components using automation, and utilizing various retention period mechanisms to control storage growth. Components of RDS PostgreSQL Storage AWS RDS PostgreSQL uses Amazon EBS volumes for storage. The limitations on the size and performance of these storage volumes are governed by AWS EBS limitations and characteristics. RDS PostgreSQL offers various configuration options for High Availability and Disaster Recovery purposes, including Multi-AZ and Multi-AZ clusters. For simplicity, we'll discuss Single-AZ RDS PostgreSQL EBS volumes. Before jumping into the RDS PostgreSQL EBS contents, let's have a look at the community PostgreSQL storage components. Once we understand the community PostgreSQL data directory architecture, we can easily understand the RDS storage components. Looking at the PostgreSQL 15 $PGDATA directory, we find the following files and directories: Most of the storage is consumed by the base, pg_wal, and log directories. The base directory contains all relational data, such as tables, indexes, and sequences. The pg_wal directory contains write-ahead log files. PostgreSQL records any modifications to data files in transactional logs called write-ahead log (WAL) files. The log directory contains database log files, which log database activities. The following diagram shows major directories with typical relative sizes in the PGDATA directory: It's worth noting that in the community PostgreSQL, you can create tablespaces for hosting objects on different storage drives. This option is not supported in RDS PostgreSQL. All data is stored in EBS volumes. With that in mind, below are the major storage components of a Single-AZ RDS instance and strategies to manage storage growth. Managing storage includes understanding retention options, automating the purging of old data, and proactively monitoring storage growth. Database Log Files All database activities are logged in RDS PostgreSQL log files. A common use case for these log files is to identify issues with database workloads, query failures, login failures, deadlocks, and fatal server errors. The size of the log files is governed by some PostgreSQL logging parameters and the RDS log file retention setting. Large log files can consume most of your RDS storage and cause production outages if the consumed storage reaches 100% of the provisioned storage. It's critical to review what you are logging and whether the logged information is needed for the business. The most important parameters that dictate the size of log files are log_statements, log_min_duration_statement, log_connections, and log_disconnections. Most fintech companies, who are required to log all user and application activities, set the most verbose option log_statement=all. This is the easiest way to bloat the log files and invite storage issues if storage consumption is not monitored. Pg_audit can be a smarter way of logging user activities, where you can specify which class of activities you want to log, such as READ, WRITE, DDL, or FUNCTION. Below is a diagram that shows the typical verbosity based on the logging parameter settings in PostgreSQL: Red: High, Orange: Medium, Green=Low One of the good practices for controlling storage used by log files is setting log file retention. The RDS parameter rds.log_retention_period sets the retention period for log files. For example, the default setting of 3 will purge log files after 3 days of their creation. Most users set it to a lower value, such as one day, and have an automated job, such as a Lambda function, configured to back up the log files to S3 as soon as they are created. Later, you can automate pgBadger to analyze these log files and send you reports at a set frequency. The following AWS CLI command can be used to find out the total size of log files in an RDS database: Plain Text aws rds describe-db-log-files --db-instance-identifier <rds_identifier> | grep "Size" | grep -o '[0-9]*' | awk '{n += $1}; END{print n}' If the total size of log files is over 25% of the total RDS used storage, it's time to build a strategy for reviewing the logging parameters and retention settings. Database Objects Database objects include regular database relations such as tables, indexes, and sequences. Below are the top factors why PostgreSQL database sizes keep increasing in an uncontrolled manner: Table Bloat When PostgreSQL deletes a row, it keeps the old version of the tuple for MVCC (multi-version concurrency control) purposes. This way, writers don't interrupt readers, and readers don't interrupt writers. Cumulatively, these dead tuples are called bloat. In PostgreSQL, an UPDATE is a combination of DELETE and INSERT. Thus, DELETE and UPDATE operations are responsible for bloat. Bloat can occur in tables or indexes. PostgreSQL hosts a native daemon process called autovacuum responsible for cleaning up this bloat and making the space available for subsequent inserts. There are some common reasons that prevent autovacuum from completing its cycle, and manual review of parameters is important. These reasons include the table being super active in use and exclusive locks blocking autovacuum jobs, long-running queries pausing autovacuum, and autovacuum_max_workers not set high enough to vacuum all bloated tables. The most optimal way to measure bloat in your database is by using the pgstattuple extension. Alternatively, you can use the official query from the PostgreSQL wiki to find a loose estimation of table bloat. The image below shows how to use pgstattuple to find the number of dead tuples. In this example, the pgbench_accounts table has a total of 200,000,000 rows and 3,263,668 dead rows, i.e., 1.44% bloat. Anything over 10% should be diagnosed, and DBAs should find the reason for the excessive bloat. The 10% factor comes from the autovacuum_vacuum_scale_factor parameter's default value in RDS PostgreSQL, which suggests autovacuum to launch if bloat goes over this value. Controlling bloat will not only control overall table size but also improve workload performance and save I/O operations. Time Series Data I have encountered many customers hosting time-series data in PostgreSQL databases. PostgreSQL offers great features for time-series data. It's important to purge old data and set the row retention period. PostgreSQL natively doesn't support TTL (Time to Live) for table rows. However, this can be achieved by using a trigger and function. The most suitable approach for handling time-series data growth is table partitioning. Table partitioning not only makes purging data easier by dropping old partitions but also makes maintenance jobs such as autovacuum more effective on large tables. Pg_cron is a suitable extension to purge old partitions at a set frequency. The command below purges data older than 10 days every midnight: SQL SELECT cron.schedule('0 0 * * *', $$DELETE FROM cron.job_run_details WHERE end_time < now() - interval '10 days'$$); Storing Unwanted Data I have met with many customers storing database logs and application logs in relational databases. The following query can be used to find the least accessed tables — the last line of the code searches for tables with less than 10 scans. SQL SELECT relname, schemaname FROM pg_stat_user_tables WHERE (coalesce(idx_tup_fetch,0) + coalesce(seq_tup_read,0)) < 10; The smarter approach could be storing application logs in cheaper storage options such as S3. Regular review is important to ensure you only store the working dataset in your relational database. Temporary Files Data resulting from large hash and sort operations that cannot fit in work_mem are stored in temporary files. Uncontrolled generation of sort and hash data can easily occupy most of the RDS PostgreSQL storage. The logging parameter log_temp_files should be enabled to log temporary file activities, and work_mem should be set to higher values. By looking at the logging details coming from the log_temp_files parameter, you can find the associated tables and queries causing most of the temporary data. One of the best practices is to set work_mem at the session level. In the EXPLAIN plan, look for the pattern: "Sort Method: external merge Disk: 47224kB". This shows that the query will create around 50MB of temporary files. In the query session, use SET LOCAL work_mem = '55MB'; to optimize query performance and lower temporary data. Conclusion In this post, we explored some strategies for storage capacity management in Amazon Relational Database Service PostgreSQL deployments. Having a close eye on the top storage contributors helps optimize storage consumption, reduces overall operational costs, and increases application performance. If you have any questions or suggestions about this post, leave a comment.
Think back to those days when you met the love of your life. The feeling was mutual. The world seemed like a better place, and you were on an exciting journey with your significant other. You were both “all-in” as you made plans for a life together. Life was amazing... until it wasn’t. When things don’t work out as planned, then you’ve got to do the hard work of unwinding the relationship. Communicating with each other and with others. Sorting out shared purchases. Moving on. Bleh. Believe it or not, our relationship with technology isn’t all that different. Breaking Up With a Service There was a time when you decided to adopt a service — maybe it was a SaaS, or a PaaS, or something more generic. Back in the day, did you make the decision while also considering the time when you would no longer plan to use the service anymore? Probably not. You were just thinking of all the wonderful possibilities for the future. But what happens when going with that service is no longer in your best interest? Now, you’re in for a challenge, and it’s called service abdication. While services can be shut down with a reasonable amount of effort, getting the underlying data can be problematic. This often depends on the kind of service and the volume of data owned by that service provider. Sometimes, the ideal unwinding looks like this: Stop paying for the service, but retain access to the data source for some period of time. Is this even a possibility? Yes, it is! The Power of VPC Peering Leading cloud providers have embraced the virtual private cloud (VPC) network as the de facto approach to establishing connectivity between resources. For example, an EC2 instance on AWS can access a data source using VPCs and VPC end-point services. Think of it as a point-to-point connection. VPCs allow us to grant access to other resources in the same cloud provider, but we can also use them to grant access to external services. Consider a service that was recently abdicated but with the original data source left in place. Here’s how it might look: This concept is called VPC peering, and it allows for a private connection to be established from another network. A Service Migration Example Let’s consider a more concrete example. In your organization, a business decision was made to streamline how it operates in the cloud. While continuing to leverage some AWS services, your organization wanted to optimize how it builds, deploys, and manages its applications by terminating a third-party, cloud-based service running on AWS. They ran the numbers and concluded that internal software engineers could stand up and support a new auto-scaled service on Heroku for a fraction of the cost that they had been paying the third-party provider. However, because of a long tenure with the service provider, migrating the data source is not an option anytime soon. You don’t want the service, and you can’t move the data, but you still want access to the data. Fortunately, the provider has agreed to a new contract to continue hosting the data and provide access — via VPC peering. Here’s how the new arrangement would look: VPC Peering With Heroku In order for your new service (a Heroku app) to access the original data source in AWS, you’ll first need to run your app within a Private Space. For more information, you can read about secure cloud adoption and my discovery of Heroku Private Spaces. Next, you’ll need to meet the following simple network requirements: The VPC must use a compatible IPv4 CIDR block in its network configuration.The VPC must use an RFC1918 CIDR block (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16).The VPC’s CIDR block must not overlap with the CIDR ranges for your Private Space. The default ranges are 10.0.0.0/16, 10.1.0.0/16, and 172.17.0.0/16. With your Private Space up and running, you’ll need to retrieve its peering information: Shell $ heroku spaces:peering:info our-new-app === our-new-app Peering Info AWS Account ID: 647xxxxxx317 AWS Region: us-east-1 AWS VPC ID: vpc-e285ab73 AWS VPC CIDR: 10.0.0.0/16 Space CIDRs: 10.0.128.0/20, 10.0.144.0/20, 10.0.0.0/20, 10.0.16.0/20 Unavailable CIDRs: 10.1.0.0/16 Copy down the AWS Account ID (647xxxxxx317) and AWS VPC ID (vpc-e285ab73). You’ll need to give that information to the third-party provider who controls the AWS data source. From there, they can use either the AWS Console or CLI to create a peering connection. Their operation would look something like this: Shell $ aws ec2 create-vpc-peering-connection \ --vpc-id vpc-e527bb17 \ --peer-vpc-id vpc-e285ab73 \ --peer-owner-id 647xxxxxx317 { "VpcPeeringConnection": { "Status": { "Message": "Initiating Request to 647xxxxxx317", "Code": "initiating-request" }, "Tags": [], "RequesterVpcInfo": { "OwnerId": "714xxxxxx214", "VpcId": "vpc-e527bb17", "CidrBlock": "10.100.0.0/16" }, "VpcPeeringConnectionId": "pcx-123abc456", "ExpirationTime": "2025-04-23T22:05:27.000Z", "AccepterVpcInfo": { "OwnerId": "647xxxxxx317", "VpcId": "vpc-e285ab73" } } } This creates a request to peer. Once the provider has done this, you can view the pending request on the Heroku side: Shell $ heroku spaces:peerings our-new-app In the screenshot below, we can see the pending-acceptance status for the peering connection. From here, you can accept the peering connection request: Shell $ heroku spaces:peerings:accept pcx-123abc456 --space our-new-app Accepting and configuring peering connection pcx-123abc456 We check the request status a second time: Shell $ heroku spaces:peerings our-new-app We see that the peer connection is active. At this point, the app running in our Heroku Private Space will be able to access the AWS data source without any issues. Conclusion An unfortunate truth in life is that relationships can be unsuccessful just as often as they can be long-lasting. This applies to people, and it applies to technology. When it comes to technology decisions, sometimes changing situations and needs drive us to move in different directions. Sometimes, things just don’t work out. And in these situations, the biggest challenge is often unwinding an existing implementation — without losing access to persistent data. Fortunately, Heroku provides a solution for slowly migrating away from existing cloud-based solutions while retaining access to externally hosted data. Its easy integration for VPC peering with AWS lets you access resources that still need to live in the legacy implementation, even if the rest of you have moved on. Taking this approach will allow your new service to thrive without an interruption in service to the consumer.
Organizations adopting Infrastructure as Code (IaC) on AWS often struggle with ensuring that their infrastructure is not only correctly provisioned but also functioning as intended once deployed. Even minor misconfigurations can lead to costly downtime, security vulnerabilities, or performance issues. Traditional testing methods — such as manually inspecting resources or relying solely on static code analysis — do not provide sufficient confidence for production environments. There is a pressing need for an automated, reliable way to validate AWS infrastructure changes before they go live. Solution Terratest provides an automated testing framework written in Go, designed specifically to test infrastructure code in real-world cloud environments like AWS. By programmatically deploying, verifying, and destroying resources, Terratest bridges the gap between writing IaC (e.g., Terraform) and confidently shipping changes. Here’s how it works: Below is a detailed guide on how to achieve AWS infrastructure testing using Terratest with Terraform, along with sample code snippets in Go. This workflow will help you provision AWS resources, run tests against them to ensure they work as intended, and then tear everything down automatically. Prerequisites Install Terraform Download and install Terraform from the official site. Install Go Terratest is written in Go, so you’ll need Go installed. Download Go from the official site. Set Up AWS Credentials Ensure your AWS credentials are configured (e.g., via ~/.aws/credentials or environment variables like AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY). Initialize a Go Module In your project directory, run: Shell go mod init github.com/yourusername/yourproject go mod tidy Add Terratest to Your go.mod In your project/repo directory, run: Shell go get github.com/gruntwork-io/terratest/modules/terraform go get github.com/stretchr/testify/assert Sample Terraform Configuration Create a simple Terraform configuration that launches an AWS EC2 instance. Put the following files in a directory named aws_ec2_example (or any name you prefer). Save it as main.tf for reference. Shell terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } required_version = ">= 1.3.0" } provider "aws" { region = var.aws_region } resource "aws_instance" "example" { ami = var.ami_id instance_type = "t2.micro" tags = { Name = "Terratest-Example" } } output "instance_id" { value = aws_instance.example.id } Next, variables.tf: Shell variable "aws_region" { type = string default = "us-east-1" } variable "ami_id" { type = string default = "ami-0c55b159cbfafe1f0" # Example Amazon Linux AMI (update as needed) } Terratest Code Snippet Create a Go test file in a directory named test (or you can name it anything, but test is conventional). For example, aws_ec2_test.go: Shell package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" ) func TestAwsEC2Instance(t *testing.T) { // Define Terraform options to point to the Terraform folder terraformOptions := &terraform.Options{ TerraformDir: "../aws_ec2_example", // Optional: pass variables if you want to override defaults Vars: map[string]interface{}{ "aws_region": "us-east-1", "ami_id": "ami-0c55b159cbfafe1f0", }, } // At the end of the test, destroy the resources defer terraform.Destroy(t, terraformOptions) // Init and apply the Terraform configuration terraform.InitAndApply(t, terraformOptions) // Fetch the output variable instanceID := terraform.Output(t, terraformOptions, "instance_id") // Run a simple assertion to ensure the instance ID is not empty assert.NotEmpty(t, instanceID, "Instance ID should not be empty") } What This Test Does Initializes and applies the Terraform configuration in ../aws_ec2_example.Deploys an EC2 instance with the specified AMI in us-east-1.Captures the instance_id Terraform output.Verifies that the instance ID is not empty using Testify’s assert library.Destroys the resources at the end of the test to avoid incurring ongoing costs. Running the Tests Navigate to the directory containing your Go test file (e.g., test directory).Run the following command: Shell go test -v Observe the output: You’ll see Terraform initializing and applying your AWS infrastructure.After the test assertions pass, Terraform will destroy the resources. Conclusion By following these steps, you can integrate Terratest into your AWS IaC workflow to: Provision AWS resources using Terraform.Test them programmatically with Go-based tests.Validate that your infrastructure is configured properly and functioning as expected.Tear down automatically, ensuring that you’re not incurring unnecessary AWS costs and maintaining a clean environment for repeated test runs.
In the world of distributed systems, few things are more frustrating to users than making a change and then not seeing it immediately. Try to change your status on your favorite social network site and reload the page only to discover your previous status. This is where Read Your Own Writes (RYW) consistency becomes quite important; this is not a technical need but a core expectation from the user's perspective. What Is Read Your Own Writes Consistency? Read Your Own Writes consistency is an assurance that once a process, usually a user, has updated a piece of data, all subsequent reads by that same process will return the updated value. It is a specific category of session consistency along the lines of how the user interacts with their own data modification. Let's look at these real-world scenarios where RYW consistency is important: 1. Social Media Updates When you tweet or update your status on your social media," is that you expect to see the tweet or status update as soon as the feed is reloaded. Without RYW consistency, content may seem to “vanish” for a brief period of time and subsequently, the same to appear multiple time, confusing your audience and duplication occurs. 2. Document Editing In systems that involve collaborative document editing, such as Google Docs, the user must see their own changes immediately, though there might be some slight delay in the updates of other users. 3. E-commerce Inventory Management If a seller updates his product inventory, he must immediately see the correct numbers in order to make informed business decisions. Common Challenges in Implementing RYW 1. Caching Complexities One of the biggest challenges comes from caching layers. When data is cached at different levels (browser, CDN, application server), it is important to have a suitable cache invalidation or update strategy so as to deliver the latest write to a client, i.e., the user. 2. Load Balancing In systems by means of multiple replicas and load balancers, requests from the same user can possibly be routed to different servers. This can break RYW consistency if not handled properly. 3. Replication Lag In primary-secondary distribution databases, writes are directed to the primary and reads can be sourced from the secondaries. All this could lead to the generation of a window where recent writes are no longer visible. Implementation Strategies 1. Sticky Sessions Python # Example load balancer configuration class LoadBalancer: def route_request(self, user_id, request): # Route to the same server for a given user session server = self.session_mapping.get(user_id) if not server: server = self.select_server() self.session_mapping[user_id] = server return server 2. Write-Through Caching Python class CacheLayer: def update_data(self, key, value): # Update database first self.database.write(key, value) # Immediately update cache self.cache.set(key, value) # Attach version information self.cache.set_version(key, self.get_timestamp()) 3. Version Tracking Python class SessionManager: def track_write(self, user_id, resource_id): # Record the latest write version for this user timestamp = self.get_timestamp() self.write_versions[user_id][resource_id] = timestamp def validate_read(self, user_id, resource_id, data): # Ensure read data is at least as fresh as user's last write last_write = self.write_versions[user_id].get(resource_id) return data.version >= last_write if last_write else True Best Practices 1. Use Timestamps or Versions Attach version information to all writesCompare versions during reads to ensure consistencyConsider using logical clocks for better ordering 2. Implement Smart Caching Strategies Use cache-aside pattern with careful invalidationConsider write-through caching for critical updatesImplement cache versioning 3. Monitor and Alert Track consistency violationsMeasure read-write latenciesAlert on abnormal patterns Conclusion Read Your Own Writes consistency may appear like a rather boring request. However, its proper implementation in a distributed system requires careful consideration of caching, routing, and data replication design issues. By being aware of the challenges involved and implementing adequate solutions, we will be able to design systems that make the experience smooth and intuitive for users. By the way, there are a lot of consistency models in distributed systems, and RYW consistency is often non-essential in the case of user experience. There is still room for users to accept eventual consistency when observing updates from other users, but they do so by expecting that their own changes will be reflected immediately.
Security comes down to trust. In DevOps and our applications, it really is a question of "should this entity be allowed to do that action?" In an earlier time in IT, we could assume that if something was inside a trusted perimeter, be it in our private network or on a specific machine, then we could assume entities were trustworthy and naturally should be able to access resources and data. However, as applications became more complex, spanning not just machines but also different data centers and continents, and reliance on third-party services via APIs became the norm, we could no longer rely on trusted perimeters. We replaced the trusted perimeter with a model based on "never trust, always verify" and "the principle of least privilege." We have come to call that model of security "zero trust," and the type of infrastructure we create using this principle "zero trust architecture." Much of the focus in zero trust discussions centers on human identities, which do need to be considered, but the challenges around securing non-human identities (NHI) should be addressed. The full scope of the NHI trust issue becomes very concerning when you consider the sheer volume involved. According to research from CyberArk, in 2022, the number of NHIs outnumbered human identities at an enterprise by a factor of 45 to 1. Some estimates put this as high as 100 to 1 in 2024 and are predicted to keep increasing well into the future. Implementing security for all our identities and leaning into zero trust has never been more important. Fortunately, you are not alone in this fight to make your applications more secure and adopt a zero-trust posture. One governing body that has put out a lot of guidance on this issue is the National Institute of Standards and Technology (NIST). In this article, we will take a closer look at achieving zero trust architecture for hour NHIs based on NIST's advice. Defining Zero Trust Architecture and NHIs Starting with an agreed-upon definition is always a good idea when contemplating any new approach or term. NIST Special Publication 800-207 gives us a formal definition of zero trust: "Zero trust (ZT) provides a collection of concepts and ideas designed to minimize uncertainty in enforcing accurate, least privilege per-request access decisions in information systems and services in the face of a network viewed as compromised. Zero trust architecture (ZTA) is an enterprise’s cybersecurity plan that utilizes zero trust concepts and encompasses component relationships, workflow planning, and access policies. Therefore, a zero trust enterprise is the network infrastructure (physical and virtual) and operational policies that are in place for an enterprise as a product of a zero trust architecture plan." Non-human identities are machine-based credentials that allow API integrations and automated workflows, which require machine-to-machine communication. These include API keys, service accounts, certificates, tokens, and roles, which collectively enable the scalability and efficiency required in modern cloud-native and hybrid environments. However, their mismanagement introduces significant security risks, making them an important component of any robust zero-trust strategy. What Can Go Wrong? Poorly managed NHIs pose significant security challenges. Secrets sprawl, leaking hardcoded API keys and tokens, often exposes sensitive credentials in codebases or logs, creating an easy target for attackers. Given the staggering number of hardcoded credentials added to public repos on GitHub alone, over 12.7 million in 2023, the majority of which were for machine identities, the full scope of this problem starts to come into focus. Adding to the issue is over-permissioned NHIs, which utilize only a fraction of their granted access, greatly expand the attack surface and heighten the risk of privilege escalation. When an attacker does find a leaked secret, they are often able to use it to laterally move throughout your systems and escalate privileges. Inadequate lifecycle management leaves stale credentials like unused service accounts and outdated certificates vulnerable. This is how the problem of "zombie leaks," when a secret is exposed but not revoked, happens in so many codebases, project management systems, and communication platforms. For example, a commit author may believe that deleting the commit or repository is sufficient, overlooking the crucial revocation step and, therefore, not completing the needed end-of-life step for managing an NHI. What NIST Has to Say About Securing NHIs NIST publishes many documents with guidance on properly securing credentials, but most of their publications focus on human identities, such as user accounts. They use the term non-person entities (NPE) in some of their work, but across the current enterprise landscape, these are much more commonly called NHI. We will stick with that current naming convention for this article. Non-human identity security consists of multiple strategies. The following points should be seen as a partial list of recommendations. Eliminate Long-Lived Credentials NIST SP 800-207: Zero Trust Architecture covers ZTA policies and emphasizes the equal treatment of NHIs and human users when it comes to authentication, authorization, and access control. One of the significant recommendations is the elimination of all long-lived credentials. By automatically expiring after a short duration, short-lived credentials reduce the risk of unauthorized access and force regular re-authentication. This ensures that any stolen or exposed credential has limited utility for attackers. Keep An Eye Out for Anomalous Activity SP 800-207 also calls for continuous monitoring of NHI Activities. Teams should strive to collect and analyze logs to detect unusual or unauthorized behavior around API calls, service account usage, or token operations. According to NIST, ZTA is especially critical in highly automated environments, such as DevOps pipelines and cloud-native architectures, where machine-to-machine interactions outnumber human actions by an increasing factor. Don't Trust for Very Long NIST SP 800-207A: "A Zero Trust Architecture Model for Access Control in Cloud-Native Applications in Multi-Cloud Environments" gives even more pointed advice. When discussing service authentication, it says, "Each service should present a short-lived cryptographically verifiable identity credential to other services that are authenticated per connection and reauthenticated regularly."Mature teams can also consider routes for replacing credentials with automatically rotated certificates. Teams already embracing service meshes can easily adopt systems like SPIFFE/SPIRE. For teams that have not already looked at PKI for machine identities, there are a lot of benefits in investigating this route. Least Privilege for NHI SP 800-207A also encourages embracing the "principle of least privilege." This ensures that NHIs operate with only the permissions necessary for their specific tasks. By minimizing access scope, organizations can significantly reduce the attack surface, limiting potential damage if an account is compromised. This requires regular audits of permissions to identify unused or excessive privileges and a continuous effort to enforce access restrictions in alignment with actual operational needs. Least privilege is particularly critical for service accounts, which often have elevated permissions by default, creating unnecessary risks in automated environments. Centralized Secrets Management Referred to in both NIST publications is a clear call for managing secrets in a centralized secrets management platform. Enterprise secret management tools such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault offer secure storage, rotation, and access control for sensitive credentials. These platforms ensure secrets are encrypted, accessed only by authorized entities, and logged for auditing purposes. By centralizing secrets management, organizations reduce the risks of secrets sprawl and mismanagement while enabling streamlined rotation policies that maintain system integrity. Securing NHIs Together NIST has provided invaluable guidance for organizations striving to adopt a zero-trust architecture and secure NHIs. Their meticulous research and recommendations, such as eliminating long-lived credentials, enforcing least privilege, and advocating for centralized secrets management, have set a strong foundation for tackling the growing complexities of securing NHIs in modern infrastructures.
We’re all familiar with the principles of DevOps: building small, well-tested increments, deploying frequently, and automating pipelines to eliminate the need for manual steps. We monitor our applications closely, set up alerts, roll back problematic changes, and receive notifications when issues arise. However, when it comes to databases, we often lack the same level of control and visibility. Debugging performance issues can be challenging, and we might struggle to understand why databases slow down. Schema migrations and modifications can spiral out of control, leading to significant challenges. Overcoming these obstacles requires strategies that streamline schema migration and adaptation, enabling efficient database structure changes with minimal downtime or performance impact. It’s essential to test all changes cohesively throughout the pipeline. Let’s explore how this can be achieved. Automate Your Tests Databases are prone to many types of failures, yet they often don’t receive the same rigorous testing as applications. While developers typically test whether applications can read and write the correct data, they often overlook how this is achieved. Key aspects like ensuring the proper use of indexes, avoiding unnecessary lazy loading, or verifying query efficiency often go unchecked. For example, we focus on how many rows the database returns but neglect to analyze how many rows it had to read. Similarly, rollback procedures are rarely tested, leaving us vulnerable to potential data loss with every change. To address these gaps, we need comprehensive automated tests that detect issues proactively, minimizing the need for manual intervention. We often rely on load tests to identify performance issues, and while they can reveal whether our queries are fast enough for production, they come with significant drawbacks. First, load tests are expensive to build and maintain, requiring careful handling of GDPR compliance, data anonymization, and stateful applications. Moreover, they occur too late in the development pipeline. When load tests uncover issues, the changes are already implemented, reviewed, and merged, forcing us to go back to the drawing board and potentially start over. Finally, load tests are time-consuming, often requiring hours to fill caches and validate application reliability, making them less practical for catching issues early. Schema migrations often fall outside the scope of our tests. Typically, we only run test suites after migrations are completed, meaning we don’t evaluate how long they took, whether they triggered table rewrites, or whether they caused performance bottlenecks. These issues often go unnoticed during testing and only become apparent when deployed to production. Another challenge is that we test with databases that are too small to uncover performance problems early. This reliance on inadequate testing can lead to wasted time on load tests and leaves critical aspects, like schema migrations, entirely untested. This lack of coverage reduces our development velocity, introduces application-breaking issues, and hinders agility. The solution to these challenges lies in implementing database guardrails. Database guardrails evaluate queries, schema migrations, configurations, and database designs as we write code. Instead of relying on pipeline runs or lengthy load tests, these checks can be performed directly in the IDE or developer environment. By leveraging observability and projections of the production database, guardrails assess execution plans, statistics, and configurations, ensuring everything will function smoothly post-deployment. Build Observability Around Databases When we deploy to production, system dynamics can change over time. CPU load may spike, memory usage might grow, data volumes could expand, and data distribution patterns may shift. Identifying these issues quickly is essential, but it's not enough. Current monitoring tools overwhelm us with raw signals, leaving us to piece together the reasoning. For example, they might indicate an increase in CPU load but fail to explain why it happened. The burden of investigating and identifying root causes falls entirely on us. This approach is outdated and inefficient. To truly move fast, we need to shift from traditional monitoring to full observability. Instead of being inundated with raw data, we need actionable insights that help us understand the root cause of issues. Database guardrails offer this transformation. They connect the dots, showing how various factors interrelate, pinpointing the problem, and suggesting solutions. Instead of simply observing a spike in CPU usage, guardrails help us understand that a recent deployment altered a query, causing an index to be bypassed, which led to the increased CPU load. With this clarity, we can act decisively, fixing the query or index to resolve the issue. This shift from "seeing" to "understanding" is key to maintaining speed and reliability. The next evolution in database management is transitioning from automated issue investigation to automated resolution. Many problems can be fixed automatically with well-integrated systems. Observability tools can analyze performance and reliability issues and generate the necessary code or configuration changes to resolve them. These fixes can either be applied automatically or require explicit approval, ensuring that issues are addressed immediately with minimal effort on your part. Beyond fixing problems quickly, the ultimate goal is to prevent issues from occurring in the first place. Frequent rollbacks or failures hinder progress and agility. True agility is achieved not by rapidly resolving issues but by designing systems where issues rarely arise. While this vision may require incremental steps to reach, it represents the ultimate direction for innovation. Metis empowers you to overcome these challenges. It evaluates your changes before they’re even committed to the repository, analyzing queries, schema migrations, execution plans, performance, and correctness throughout your pipelines. Metis integrates seamlessly with CI/CD workflows, preventing flawed changes from reaching production. But it goes further — offering deep observability into your production database by analyzing metrics and tracking deployments, extensions, and configurations. It automatically fixes issues when possible and alerts you when manual intervention is required. With Metis, you can move faster and automate every aspect of your CI/CD pipeline, ensuring smoother and more reliable database management. Everyone Needs to Participate Database observability is about proactively preventing issues, advancing toward automated understanding and resolution, and incorporating database-specific checks throughout the development process. Relying on outdated tools and workflows is no longer sufficient; we need modern solutions that adapt to today’s complexities. Database guardrails provide this support. They help developers avoid creating inefficient code, analyze schemas and configurations, and validate every step of the software development lifecycle within our pipelines. Guardrails also transform raw monitoring data into actionable insights, explaining not just what went wrong but how to fix it. This capability is essential across all industries, as the complexity of systems will only continue to grow. To stay ahead, we must embrace innovative tools and processes that enable us to move faster and more efficiently.
API testing has gained a lot of momentum these days. As UI is not involved, it is a lot easier and quicker to test. This is the reason why API testing is considered the first choice for performing end-to-end testing of the system. Integrating the automated API Tests with the CI/CD pipelines allows teams to get faster feedback on the builds. In this blog, we'll discuss and learn about DELETE API requests and how to handle them using Playwright Java for automation testing, covering the following points: What is a DELETE request?How do you test DELETE APIs using Playwright Java? Getting Started It is recommended that you check out the earlier tutorial blog to learn about the details related to prerequisites, setup, and configuration. Application Under Test We will be using the free-to-use RESTful e-commerce APIs that offer multiple APIs related to order management functionality, allowing us to create, retrieve, update, and delete orders. This application can be set up locally using Docker or NodeJS. What Is a DELETE Request? A DELETE API request deletes the specified resource from the server. Generally, there is no response body in the DELETE requests. The resource is specified by a URI, and the server permanently deletes it. DELETE requests are neither considered safe nor idempotent, as they may cause side effects on the server, like removing data from a database. The following are some of the limitations of DELETE requests: The data deleted using a DELETE request is not reversible, so it should be handled carefully.It is not considered to be a safe method as it can directly delete the resource from the database, causing conflicts in the system.It is not an idempotent method, meaning calling it multiple times for the same resource may result in different states. For example, in the first instance, when DELETE is called, it will return Status Code 204 stating that the resource has been deleted, and if DELETE is called again on the same resource, it may give a 404 NOT FOUND as the given resource is already deleted. The following is an example of the DELETE API endpoint from the RESTful e-commerce project. DELETE /deleteOrder/{id} : Deletes an Order By ID This API requires the order_id to be supplied as Path Parameter in order to delete respective order from the system. There is no request body required to be provided in this DELETE API request. However, as a security measure, the token is required to be provided as a header to delete the order. Once the API is executed, it deletes the specified order from the system and returns Status Code 204. In case where the order is not found, or the token is not valid or not provided, it will accordingly show the following response: Status CodeDescription400 Failed to authenticate the token404 No order with the given order_id is found in the system403Token is missing in the request How to Test DELETE APIs Using Playwright Java Testing DELETE APIs is an important step in ensuring the stability and reliability of the application. Correct implementation of the DELETE APIs is essential to check for unintended data loss and inconsistencies, as the DELETE APIs are in charge of removing the resources from the system. In this demonstration of testing DELETE APIs using Playwright Java, we'll be using the /deleteOrder/{id} for deleting an existing order from the system. Test Scenario 1: Delete a Valid Order Start the RESTful e-commerce service.Using a POST request, create some orders in the system.Delete the order with order_id “1” using DELETE request.Check that the Status Code 204 is returned in the response. Test Implementation The following steps are required to be performed to implement the test scenario: Add new orders using the POST request.Hit the /auth API to generate token.Hit the /deleteOrder/ API endpoint with the token and the order_id to delete the order.Check that the Status Code 204 is returned in the response. A new test method, testShouldDeleteTheOrder(), is created in the existing test class HappyPathTests. This test method implements the above three steps to test the DELETE API. Java @Test public void testShouldDeleteTheOrder() { final APIResponse authResponse = this.request.post("/auth", RequestOptions.create().setData(getCredentials())); final JSONObject authResponseObject = new JSONObject(authResponse.text()); final String token = authResponseObject.get("token").toString(); final int orderId = 1; final APIResponse response = this.request.delete("/deleteOrder/" + orderId, RequestOptions.create() .setHeader("Authorization", token)); assertEquals(response.status(), 204); } The POST /auth API endpoint will be hit first to generate the token. The token received in response is stored in the token variable to be used further in the DELETE API request. Next, new orders will be generated using the testShouldCreateNewOrders() method, which is already discussed in the previous tutorial, where we talked about testing POST requests using Playwright Java. After the orders are generated, the next step is to hit the DELETE request with the valid order_id that would delete the specific order. We'll be deleting the order with the order_id “1” using the delete() method provided by Playwright framework. After the order is deleted, the Status Code 204 is returned in response. An assertion will be performed on the Status Code to verify that the Delete action was successful. Since no request body is returned in the response, this is the only thing that can be verified. Test Execution We'll be creating a new testng.xml named testng-restfulecommerce-deleteorders.xml to execute the tests in the order of the steps that we discussed in the test implementation. XML <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Restful ECommerce Test Suite"> <test name="Testing Happy Path Scenarios of Creating and Updating Orders"> <classes> <class name="io.github.mfaisalkhatri.api.restfulecommerce.HappyPathTests"> <methods> <include name="testShouldCreateNewOrders"/> <include name="testShouldDeleteTheOrder"/> </methods> </class> </classes> </test> </suite> First, the testShouldCreateNewOrders() test method will be executed, and it will create new orders. Next, the testShouldDeleteTheOrder() test method order will be executed to test the delete order API. The following screenshot of the test execution performed using IntelliJ IDE shows that the tests were executed successfully. Now, let’s verify that the order was correctly deleted by writing a new test that will call the GET /getOrder API endpoint with the deleted order_id. Test Scenario 2: Retrieve the Deleted Order Delete a valid order with order_id “1.”Using GET /getOrder API, try retrieving the order with order_id “1.”Check that the Status Code 404 is returned with the message “No Order found with the given parameters!” in the response. Test Implementation Let’s create a new test method, testShouldNotRetrieveDeletedOrder(), in the existing class HappyPathTests. Java @Test public void testShouldNotRetrieveDeletedOrder() { final int orderId = 1; final APIResponse response = this.request.get("/getOrder", RequestOptions.create().setQueryParam("id", orderId)); assertEquals(response.status(), 404); final JSONObject jsonObject = new JSONObject(response.text()); assertEquals(jsonObject.get("message"), "No Order found with the given parameters!"); } The test implementation of this scenario is pretty simple. We will be executing the GET /getOrder API and to fetch the deleted order with order_id “1.” An assertion is applied next to verify that the GET API should return the Status Code 404 in the response with the message “No Order found with the given parameters!” This test ensures that the delete order API worked fine and the order was deleted from the system. Test Execution Let’s update the testng.xml file and add this test scenario at the end after the delete test. Java <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Restful ECommerce Test Suite"> <test name="Testing Happy Path Scenarios of Creating and Updating Orders"> <classes> <class name="io.github.mfaisalkhatri.api.restfulecommerce.HappyPathTests"> <methods> <include name="testShouldCreateNewOrders"/> <include name="testShouldDeleteTheOrder"/> <include name="testShouldNotRetrieveDeletedOrder"/> </methods> </class> </classes> </test> </suite> Now, all three tests should run in sequence. The first one will create orders; the second one will delete the order with order_id “1”; and the last test will hit the GET API to fetch the order with order_id “1” returning Status Code 404. The screenshot above shows that all three tests were executed successfully, and the DELETE API worked fine as expected. Summary DELETE API requests allow the deletion of the resource from the system. As delete is an important CRUD function, it is important to test it and verify that the system is working as expected. However, it should be noted that DELETE is an irreversible process, so it should always be used with caution. As per my experience, it is a good approach to hit the GET API after executing the DELETE request to check that the specified resource was deleted from the system successfully. Happy testing!
Should Programmers Solve Business Problems?
January 10, 2025 by
Top 5 Books to Enhance Your Software Design Skills in 2025
January 10, 2025 by CORE
Top Mistakes Made by IT Architects
January 9, 2025 by
Revolutionizing Catalog Management for Data Lakehouse With Polaris Catalog
January 10, 2025 by
Optimizing SQL Server Performance With AI: Automating Query Optimization and Predictive Maintenance
January 10, 2025 by CORE
Top 5 Books to Enhance Your Software Design Skills in 2025
January 10, 2025 by CORE
Low-Maintenance Backend Architectures for Scalable Applications
January 10, 2025 by
Building a Sample Kubernetes Operator on Minikube: A Step-by-Step Guide
January 10, 2025 by CORE
Optimizing SQL Server Performance With AI: Automating Query Optimization and Predictive Maintenance
January 10, 2025 by CORE
Metaprogramming With Proxies and Reflect in JavaScript
January 10, 2025 by
Mastering macOS Client-Server Application Testing: Tools and Key Differences
January 10, 2025 by
Building a Sample Kubernetes Operator on Minikube: A Step-by-Step Guide
January 10, 2025 by CORE
Mastering macOS Client-Server Application Testing: Tools and Key Differences
January 10, 2025 by
Building a Sample Kubernetes Operator on Minikube: A Step-by-Step Guide
January 10, 2025 by CORE
Top 5 Books to Enhance Your Software Design Skills in 2025
January 10, 2025 by CORE
Metaprogramming With Proxies and Reflect in JavaScript
January 10, 2025 by
Optimizing SQL Server Performance With AI: Automating Query Optimization and Predictive Maintenance
January 10, 2025 by CORE
Maximizing AI Agents for Seamless DevOps and Cloud Success
January 9, 2025 by CORE