Welcome to the Data Engineering category of DZone, where you will find all the information you need for AI/ML, big data, data, databases, and IoT. As you determine the first steps for new systems or reevaluate existing ones, you're going to require tools and resources to gather, store, and analyze data. The Zones within our Data Engineering category contain resources that will help you expertly navigate through the SDLC Analysis stage.
Artificial intelligence (AI) and machine learning (ML) are two fields that work together to create computer systems capable of perception, recognition, decision-making, and translation. Separately, AI is the ability for a computer system to mimic human intelligence through math and logic, and ML builds off AI by developing methods that "learn" through experience and do not require instruction. In the AI/ML Zone, you'll find resources ranging from tutorials to use cases that will help you navigate this rapidly growing field.
Big data comprises datasets that are massive, varied, complex, and can't be handled traditionally. Big data can include both structured and unstructured data, and it is often stored in data lakes or data warehouses. As organizations grow, big data becomes increasingly more crucial for gathering business insights and analytics. The Big Data Zone contains the resources you need for understanding data storage, data modeling, ELT, ETL, and more.
Data is at the core of software development. Think of it as information stored in anything from text documents and images to entire software programs, and these bits of information need to be processed, read, analyzed, stored, and transported throughout systems. In this Zone, you'll find resources covering the tools and strategies you need to handle data properly.
A database is a collection of structured data that is stored in a computer system, and it can be hosted on-premises or in the cloud. As databases are designed to enable easy access to data, our resources are compiled here for smooth browsing of everything you need to know from database management systems to database languages.
IoT, or the Internet of Things, is a technological field that makes it possible for users to connect devices and systems and exchange data over the internet. Through DZone's IoT resources, you'll learn about smart devices, sensors, networks, edge computing, and many other technologies — including those that are now part of the average person's daily life.
One Database, Two Models: Building AI-Friendly Java Applications With MySQL JSON Duality Views
Getting Hands-On With Monitoring AI Agents and Models in Gemini Enterprise Agent Platform
Most gains in a supervised learning project don't come from switching algorithms; they come from what you feed the algorithm. Feature engineering is the process of transforming raw data into representations that make patterns easier for a model to learn. Done well, it routinely closes more of the accuracy gap than hyperparameter tuning or trying a fancier model. This article walks through the feature engineering techniques that consistently move the needle in real projects, with working Python code (pandas + scikit-learn) and a visual look at where the gains actually come from. Figure 1: Where feature engineering sits in a typical supervised ML pipeline. Why Feature Engineering Beats Model Selection (Often) A gradient-boosted tree with well-engineered features will usually outperform a deep neural network fed raw, unprocessed columns. That's because feature engineering directly encodes domain knowledge and statistical structure that the model would otherwise have to infer from data alone — often with far more examples than you have available. Figure 2: Cumulative accuracy gains on a churn-prediction task as each feature engineering step is added. In this example (a customer churn classifier trained with gradient-boosted trees), accuracy climbs from 0.712 on raw features to 0.851 after five feature engineering passes, a 13.9-point improvement without changing the model or its hyperparameters. Techniques That Actually Move the Needle 1. Missing Value Imputation (With an Indicator Flag) Dropping rows with missing values throws away signal. Simple mean/median imputation is a safe default but pairing it with a binary "was this missing" flag preserves the fact that the value was absent, which is often itself predictive (e.g., a missing income field can correlate with churn). Python import pandas as pd from sklearn.impute import SimpleImputer # Flag missingness before imputing — the flag is a feature in its own right df['income_missing'] = df['income'].isna().astype(int) imputer = SimpleImputer(strategy='median') df['income'] = imputer.fit_transform(df[['income']]) 2. Categorical Encoding (Target/Frequency, Not Just One-Hot) One-hot encoding works fine for low-cardinality columns, but it explodes dimensionality for high-cardinality categoricals (zip codes, product IDs). Target encoding, replacing a category with the mean target value for that category, keeps dimensionality flat and often outperforms one-hot for tree-based models. Always fit encodings on the training fold only to avoid leakage. Python from category_encoders import TargetEncoder from sklearn.model_selection import KFold encoder = TargetEncoder(cols=['zip_code'], smoothing=0.3) # Fit only on training data, then apply to validation/test encoder.fit(X_train['zip_code'], y_train) X_train['zip_code_enc'] = encoder.transform(X_train['zip_code']) X_valid['zip_code_enc'] = encoder.transform(X_valid['zip_code']) 3. Scaling and Skew Correction Distance-based and linear models (KNN, SVM, logistic regression) are sensitive to feature scale; tree-based models are not, but still benefit from skew correction. A log transform on heavily right-skewed features (income, transaction amounts) tightens the distribution and reduces the influence of outliers. Python import numpy as np from sklearn.preprocessing import StandardScaler # Log-transform skewed monetary features (add 1 to handle zeros) df['txn_amount_log'] = np.log1p(df['txn_amount']) scaler = StandardScaler() num_cols = ['age', 'tenure_months', 'txn_amount_log'] df[num_cols] = scaler.fit_transform(df[num_cols]) 4. Interaction and Ratio Features Models capture linear or split-based patterns per feature; they don't automatically discover that the ratio of two columns is what matters. Manually creating interaction terms and ratios spend-per-visit, tenure-to-age ratio gives the model shortcuts to patterns it would otherwise need many more splits or much more data to approximate. Python # Ratio feature: average spend per visit is often more predictive # than either 'total_spend' or 'visit_count' alone df['spend_per_visit'] = df['total_spend'] / df['visit_count'].replace(0, 1) # Interaction: tenure relative to customer age df['tenure_ratio'] = df['tenure_months'] / (df['age'] * 12) 5. Date/Time Decomposition A raw timestamp is nearly useless to a model. Decomposing it into cyclical and calendar components day of week, month, is-weekend, sine/cosine encodings for cyclical features exposes seasonality and recurring patterns that the model can actually split on. Python df['signup_date'] = pd.to_datetime(df['signup_date']) df['signup_dow'] = df['signup_date'].dt.dayofweek df['signup_month'] = df['signup_date'].dt.month df['is_weekend'] = (df['signup_dow'] >= 5).astype(int) # Cyclical encoding so December and January are 'close' to the model df['month_sin'] = np.sin(2 * np.pi * df['signup_month'] / 12) df['month_cos'] = np.cos(2 * np.pi * df['signup_month'] / 12) 6. Feature Selection (Prune After You Expand) Adding interaction and encoded features inflates dimensionality, which can reintroduce noise and overfitting. Closing the loop with feature selection, dropping low-importance or highly correlated features, is what typically delivers the last, and sometimes largest, accuracy gain in the pipeline. Python from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import GradientBoostingClassifier base_model = GradientBoostingClassifier(random_state=42) base_model.fit(X_train, y_train) # Keep only features with above-median importance selector = SelectFromModel(base_model, threshold='median', prefit=True) X_train_selected = selector.transform(X_train) X_valid_selected = selector.transform(X_valid) print(f'Kept {X_train_selected.shape[1]} of {X_train.shape[1]} features') Common pitfall: fitting imputers, encoders, and scalers on the full dataset before splitting into train/test. This leaks information from the validation set into training and inflates your reported accuracy. Always fit transformations on the training fold only, inside a cross-validation loop or an sklearn Pipeline. Putting It Together: Before vs. After The table below summarizes the cumulative effect of each technique from the churn-prediction example, holding the model (gradient boosted trees, default hyperparameters) constant throughout. steptechnique addedaccuracyΔ vs. Baseline 0 Baseline (raw features) 0.712 — 1 Missing value imputation + flag 0.741 +2.9 pts 2 Target encoding for categoricals 0.768 +5.6 pts 3 Log transform + scaling 0.781 +6.9 pts 4 Interaction/ratio features 0.824 +11.2 pts 5 Feature selection (prune to median importance) 0.851 +13.9 pts Wrapping It in a Reproducible Pipeline Scattered feature engineering code is a common source of train/serve skew. Wrapping the steps in an sklearn Pipeline (or ColumnTransformer) keeps fitting and transforming consistent between training and inference, and makes the whole thing a single object you can version and deploy. Python from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler from category_encoders import TargetEncoder from sklearn.ensemble import GradientBoostingClassifier numeric_features = ['age', 'tenure_months', 'txn_amount_log'] categorical_features = ['zip_code'] preprocessor = ColumnTransformer(transformers=[ ('num', Pipeline([ ('impute', SimpleImputer(strategy='median')), ('scale', StandardScaler()), ]), numeric_features), ('cat', TargetEncoder(), categorical_features), ]) full_pipeline = Pipeline([('preprocess', preprocessor), ('model', GradientBoostingClassifier(random_state=42)), ]) full_pipeline.fit(X_train, y_train) print(full_pipeline.score(X_valid, y_valid)) Best Practices Checklist Always fit transformers (imputers, encoders, scalers) on the training fold only, never on the full dataset before splitting.Flag missingness explicitly instead of silently imputing; the flag itself often carries signal. Prefer target/frequency encoding over one-hot for high-cardinality categorical columns. Log-transform heavily skewed numeric features before scaling. Create interaction and ratio features that reflect domain knowledge, not random combinations. Decompose timestamps into calendar and cyclical components rather than using raw dates. Re-run feature selection after adding new engineered features to control dimensionality and overfitting. Wrap the whole process in a pipeline/column transformer so training and inference stay in sync. Conclusion Feature engineering is unglamorous compared to trying the newest architecture, but it's usually the highest-leverage work in a supervised learning project. Handle missing data deliberately, encode categoricals to fit their cardinality, correct skew, build interactions that reflect real relationships in your domain, decompose dates, and prune what you don't need. In the example walked through here, that sequence alone was responsible for nearly 14 points of accuracy, with the model and hyperparameters held constant the entire time.
The City Traffic Analogy Imagine a city's traffic system during rush hour. A minor accident blocks one lane on a major highway. Traffic slows down, but the road remains partially functional. Now imagine every driver, upon encountering the slowdown, decides to: Take an alternate route through residential streetsCircle back and try the highway againCall friends to warn them, who then also reroute What happens? The residential streets become congested. Side roads jam up. Intersections gridlock. The alternate routes become slower than the original highway. Drivers circle back, creating even more congestion. What started as a single blocked lane cascades into city-wide traffic paralysis. The problem isn't the initial accident. The problem is the collective response to it. This is exactly what happens in distributed systems when retry logic becomes aggressive during degradation. A slow downstream service is like a partially blocked highway. Blind retries are like drivers flooding alternate routes without coordination. The recovery mechanism — designed to help — amplifies the original problem. Traffic engineers solve this with adaptive signals, ramp metering to control highway entry, real-time routing that reflects current conditions, and occasionally closing certain routes entirely to let the system recover. Distributed systems need the same discipline: not just a mechanism to retry, but a policy for when to hold back. The Illusion of "Retry Until It Works" Distributed systems plan for failure using timeouts, circuit breakers, message queues, and dead-letter topics. Among these tools, retries are the most common because the logic seems simple: A call fails.Wait a moment.Try again. For an isolated network drop or server reboot, this works well. But when a dependency struggles under heavy load, a retry is never free. Every retry: Sends another request over the wire.Consumes worker or execution capacity while the request is in flight.Consumes connection-pool capacity while the request remains in flight.Adds another item to process in the queue. When thousands of clients hit the same hiccup and retry together, they create an accidental traffic storm. The recovery code stops being a safety net and becomes the main driver of the outage. How Small Glitches Snowball Consider a hypothetical event platform delivering millions of webhook notifications every hour. Suppose a downstream API slows down under sudden pressure. Instead of returning in 50 milliseconds, responses stretch to 4 seconds. Some calls time out; others return 503 errors. In this scenario, if 100,000 requests fail and each client is configured for 4 retries, the recovery mechanism can generate up to 400,000 additional retry attempts against a target that is already drowning. Plain Text Downstream slows down │ ▼ Requests time out │ ▼ Clients retry │ ▼ Extra inbound traffic │ ▼ Higher downstream latency │ ▼ Widespread failure │ ▼ Retry storm This creates a self-reinforcing loop: the system reacts to failure by generating more work, adding latency, triggering more timeouts, and generating even more retries. The Six-Layer Amplification Framework Most engineers view retry amplification as simple request volume—turning one failed request into several. In reality, the blast radius cuts across multiple infrastructure layers: Plain Text Request Amplification ──► 1 failed call multiplies into 3–5x outbound requests Worker Amplification ──► Saturated execution capacity reduces fresh-work capacity Connection Amplification ──► In-flight requests consume connection-pool capacity Queue Amplification ──► Repeated redelivery increases queue pressure and lag Latency Amplification ──► Shared resource contention slows healthy paths Neighbor Amplification ──► Failing targets starve unrelated platform tenants Requests: One failing transaction multiplies into multiple outgoing calls.Workers: Long-running retries tie up execution capacity, reducing throughput for fresh work.Connections: In-flight requests consume connection-pool capacity while waiting on responses.Queues: Repeated processing and redelivery increase queue pressure and can drive consumer lag.Latency: Resource contention across shared CPU and network paths slows down healthy endpoints.Neighbors: A single failing downstream target hogs shared platform resources, degrading unrelated tenants. The important insight is that retry amplification is not merely a traffic problem; it is a resource-propagation problem. Why Queues Cannot Prevent the Storm Message queues like Kafka, RabbitMQ, and AWS SQS provide durable buffering and asynchronous decoupling, helping systems preserve messages while consumers are unavailable or degraded. However, a queue answers one specific question: "Is the message saved safely in persistent storage?" It cannot answer: "Is it operationally safe to deliver this message right now?" If workers blindly pull messages off a topic and blast them against an overloaded API, the queue acts like a firehose pointed at a fragile target. Queues provide buffering; they do not determine whether delivery should happen now. Failure Is Not Just "Up" or "Down" Basic retry policies assume a binary world: a service is either working or offline. Real production outages are rarely that clean. A downstream dependency might be: Returning 200s, but running ten times slower than normal.Dropping 20% of requests because its database connection pool is full.Rejecting calls because memory crossed a critical limit.Throttling traffic because a tenant hit their API rate limit. If your system treats a slow, memory-starved server like a brief network blip, it will keep hammering that server until it crashes. Matching the Recovery to the Failure Different failure modes require completely different responses: Transient Blips: Dropped packets or DNS timeouts. Retry with exponential backoff and randomized jitter so clients do not hit the server simultaneously.Rate Limits (HTTP 429): The service asks you to slow down. Read Retry-After, back off, cut concurrency, and resume gradually.Downstream Overload: Requests time out under load. Trip circuit breakers, lower concurrency, and pause retries until latency recovers.Non-Retryable Client Errors: Invalid payloads, unsupported operations, or permanent business-rule rejections should not be retried. Route them directly to a dead-letter queue (DLQ) for inspection. Knowing When to Stop Resilient architectures know when doing nothing is safer than trying again. SituationUnsafe ResponseSafe ResponseHTTP 429 Rate LimitImmediate retry loopThrottle affected traffic and respect the limit Retry-AfterCircuit Breaker OpenKeep pinging targetFast-fail or park messages in DLQHigh Queue BacklogRetry all failed jobsDrop or delay low-priority retriesInvalid Message SchemaRetry 5 timesRoute directly to DLQ without retryingDownstream Latency > SLAFire aggressive retriesCut concurrency and shed non-critical load Adaptive Recovery: Check System State First A static policy like retries = 3, backoff = 2s has no awareness of the surrounding environment. Consider two situations handling the same error: Scenario A: Queue depth is 150, CPU is at 20%, downstream latency is 40ms. Retrying is safe.Scenario B: Queue depth is 700,000, workers are 98% busy, downstream latency is 9s. Retrying will trigger an outage. A resilient system checks platform health before firing another request: Plain Text Request Fails │ ▼ Check Failure Type ──► Permanent error? ──► Send to DLQ │ (Transient) ▼ Check Downstream Health (Circuit breaker / Latency) │ (Degraded) ▼ Check Local Load (Queue depth / Worker pool) │ ├─► System healthy ──► Exponential backoff + Jitter ──► Retry │ └─► System stressed ─► Park message / Throttle rate ─► Delay Protecting Shared Resources To keep one broken dependency from pulling down the entire platform: Enforce a Retry Budget: Limit the capacity recovery traffic can consume based on normal workload traffic and available system headroom. When exhausted, delay, throttle, or park retries.Isolate Worker Pools: Process retries on dedicated worker pools so they cannot starve fresh incoming requests.Partition by Target: Apply distinct concurrency limits per downstream endpoint so healthy routes run unaffected.Prioritize Critical Work: When queues back up, retry critical jobs (payments, security alerts) and defer low-priority ones (analytics).Warm Up Slowly: When a failed service recovers, release queued traffic in controlled batches rather than dumping the backlog all at once. What to Measure in Production Standard metrics like error rate and average CPU hide retry problems until damage is done. Track these indicators instead: Retry-to-Primary Ratio: Track how much outgoing capacity is consumed by retries. A sustained increase signals that recovery traffic is becoming a significant source of system load.Retries per Recovery: How many attempts does a message need before succeeding? Rising numbers mean wasted compute.Worker Occupancy: How much worker capacity is spent processing retries instead of fresh work?Circuit Breaker Trip Rate: How often and how long breakers stay open across each downstream target. Summary: Recovery Has a Cost Retrying is not just an error-handling convenience — it is a resource allocation decision. Every recovery attempt consumes bandwidth, threads, memory, and downstream capacity. When systems fail under load, retrying harder often becomes the primary mechanism of collapse. To build genuinely resilient distributed architectures: Treat recovery actions as operations with real infrastructure costs.Filter errors early so permanent failures are never retried.Respect downstream throttling signals and use circuit breakers when defined failure or latency thresholds are reached.Use randomized jitter and exponential backoff to prevent self-inflicted traffic spikes.Check operational system state before deciding whether another attempt will help or harm. True system resilience is not about trying indefinitely until a call succeeds. It is about recognizing that recovery consumes resources, and ensuring every recovery decision accounts for system state before taking action.
All database administrators have experienced something like this at one point. A query that was running well suddenly starts to create problems. No error, failure, or warning appears on the dashboard, and the only noticeable sign is users complaining that the application is running slowly today. Now the DBA must determine which of the countless queries is causing the problem. The best solution here is the Query Store tool. Since its implementation in 2016, this special feature has been storing data about SQL query text, execution plans, and statistics. In addition, unlike the plan cache, it retains this information even after a server restart. However, the trick is to get this data in a user-friendly format. Looking is easy. Automating is the hard part. When you launch SSMS, you will see the built-in "Regressed Queries" report — a GUI pane you open, filter by a metric and time window, and read. It's genuinely useful for firefighting: when something's slow right now, it's a fast way to see what changed. There are scriptable options too. Erik Darling's free, open-source sp_QuickieStore is a stored procedure that digs into Query Store and hands back result sets you can filter in ways the GUI won't let you. It's excellent, and if you're comfortable in T-SQL, it's well worth knowing. But notice what both leave on the table. The built-in report is something you open and stare at — fine for one database in the moment, useless for checking 150 of them every morning unattended. And even a scriptable stored procedure returns a result set for you to look at, not an object you can pipe onward. What most of us actually want is a job that runs autonomously, checks the whole estate overnight, and hands the result straight to a report or an alert without anyone pressing buttons. This is the gap. Plenty of tools let you see regressions; a stored procedure lets you query them. Far fewer let you script them as objects — things you can filter, sort, pipe, and schedule as part of a pipeline. Once you're working with objects instead of screens or result sets, a lot becomes possible. What Actually Counts as a Regression Before initiating the automation process, it's wise to be truthful about what you're measuring, since the simple way is often misleading. The obvious approach is "compare yesterday's average duration to today's." Two things are wrong with that. First, a plain average hides spikes — a query that's usually fast but occasionally awful gets smoothed into looking fine. Second, and worse, it treats every query as equal. A query that ran twice yesterday and a query that ran fifty thousand times yesterday both get one vote, so a query that happened to be slow on its two executions shows up as a giant "regression" next to nothing, and drowns out the query that quietly got 30% slower across fifty thousand real executions and is actually hurting you. The fix is to weight by how often a query actually runs. Give the fifty-thousand-execution query the influence its volume earns, and filter out the low-count noise that produces false alarms. You're not asking "did the average change," you're asking "did something people actually run a lot get meaningfully slower." That's the difference between a signal you trust and an inbox full of cries of wolf. One should also consider the lower limit of the two parameters chosen for the comparison. One might think it is enough to set the criterion of "servicing at least N times not long ago." However, even if the baseline was executed only a few times, it is still not a reliable indicator for comparison. Doing It in PowerShell This is the basis for Find-SqlQueryStoreRegression — the command in SqlReliabilityKit, a small command-line module available for free under the MIT License that provides a mechanism for reliability analysis. The command processes Query Store DMVs, executes a weighted baseline, removes any irrelevant noise, and produces output in the form of results. PowerShell Install-Module SqlReliabilityKit -Scope CurrentUser Queries that ran meaningfully slower in the recent window vs. the baseline window: PowerShell Find-SqlQueryStoreRegression -SqlInstance localhost -Database QsTest It is not editable. It only reads the Query Store and generates results; it neither changes plans nor performs any modifications of its settings. What you do with that information is up to you, which is the whole point. As the results are presented as various objects, the most interesting part is the pipe. The ten worst regressions, by how many times slower they got: PowerShell Find-SqlQueryStoreRegression -SqlInstance localhost -Database QSTest | Sort-Object SlowdownFactor -Descending | Select-Object -First 10 Only the serious ones, on queries that actually run a lot: PowerShell Find-SqlQueryStoreRegression ` -SqlInstance localhost ` -Database QSTest ` -SlowdownThreshold 2.0 ` -MinExecutionCount 50 That second one reads as plainly as it runs: show me queries that at least doubled, and only ones that ran fifty-plus times so I'm not chasing noise. Schedule that, pipe the result to an email or a Teams webhook, and you've turned "stare at a dashboard" into "get told when something's wrong." Watching It Actually Catch One One is justified in doubting tools that claim to spot issues. A tool that always fails to go off in trials is simply a tool that has not had its lies caught yet. So here is how you can ensure that your device will catch regressions that you will cause intentionally — and that is also the only honest way for you to trust any monitoring device. The trick is to take a query that's fast because it uses an index, then pull the index out from under it: Baseline: Run a query many times while it's fast (index present). PowerShell 1..300 | ForEach-Object { Invoke-DbaQuery -SqlInstance localhost -Database QSTest -Query "SELECT COUNT(*) FROM dbo.Orders WHERE CustomerId = $($_ % 5000)" } Now break it: Drop the index so the same query scans the whole table. PowerShell Invoke-DbaQuery -SqlInstance localhost -Database QSTest -Query "DROP INDEX IX_Orders_CustomerId ON dbo.Orders" Run it slow, flush Query Store, then check 300 more executions, then: PowerShell Find-SqlQueryStoreRegression ` -SqlInstance localhost ` -Database QSTest ` -WindowUnit Minute ` -BaselineStart 15 ` -BaselineEnd 3 ` -MinExecutionCount 10 ` -SlowdownThreshold 1.5 | Format-Table -AutoSize The query text is the same. Only the cost varies now because we are using the same plan, which now scans a couple hundred thousand rows instead of seeking. Therefore, we see the same query in the output, along with a baseline of the milliseconds it would take to complete the same request compared with the current milliseconds spent. This is exaggerated, for sure, since in real life we see no more than 2 or 10 times. However, this proves that the detection works for the appropriate query. One more detail in the output is worth noting. It shows whether the plan changed during the process. In this case, it did not change, as there was no need. All that we did was drop the index; it did not change the plan, but it made it more expensive in terms of cost. This is why it is important to note that the two situations differ. The Point The Query Store is already familiar with your performance history. The problem has never been data. The issue has always been finding a way to convert data into actionable information that can be used without human intervention. The first step is giving weights according to executions so that the volume matters, making sure both windows are floored so that the noise does not matter. Next, return objects instead of a downloadable report. This makes the difference between investigating which query failed at 2 o’clock in the morning and adding this process to a scheduled job. Find-SqlQueryStoreRegression is one command in an early, actively developed module, so feedback and issues are genuinely welcome — SqlReliabilityKit on the PowerShell Gallery, source on GitHub.
Over the past year, I have sat in enough leadership conversations to notice a pattern. A CTO tells me their organization has "adopted AI" because most of the engineering team now has access to Copilot or ChatGPT, and everyone sat through a lunch-and-learn on prompting. A few months later, the excitement usually settles into something quieter: a handful of engineers using AI daily, one or two pilots that never left the sandbox, and no clear answer to what actually changed operationally. The problem isn't that these tools don't work. It's that adoption is being measured by the wrong thing. Counting how many people have access to an AI chatbot tells you almost nothing about how deeply AI has been woven into the way work gets done. A team of ten people where AI reliably runs one integrated production workflow is arguably further along than a company of a hundred where everyone occasionally pastes text into a chatbot. I want to offer a different way of thinking about this: a five-stage model that measures AI adoption by responsibility, integration, reliability, and control, rather than headcount. StageNameWhat changes1Individual AssistanceAI helps a person perform an isolated task2Workflow AssistanceAI becomes part of repeatable development activities3Integrated AutomationAI connects with applications, APIs, data, and workflows4Agentic OperationsAI can perform multi-step actions with tools5Governed AI-Native DeliveryAI becomes operational infrastructure with controls, monitoring, and measurable outcomes Why Tool Adoption Is Not AI Maturity There's a meaningful difference between AI access, AI usage, AI integration, AI operational responsibility, and AI maturity, and most conversations about "adoption" collapse all five into one. A developer asking an assistant to explain an unfamiliar function is AI usage. It's genuinely useful, but it ends the moment the conversation window closes. Compare that with a system that automatically inspects a failed deployment, pulls the relevant logs, produces a diagnosis, opens a ticket, and escalates it to the right engineer. That's operating at a completely different level, even though both examples technically involve "AI." This is why I think the unit of measurement should increasingly be the workflow rather than the user. I have watched this same mistake happen with other categories of technology: businesses buying automation platforms, CRM systems, or new SaaS tools and layering them on top of an existing process without redesigning how the work actually gets done. The tool changes. The process doesn't. AI risks following the same pattern, just with better marketing behind it. Stage 1: Individual Assistance Most organizations start here, and there's nothing wrong with that. Stage 1 covers code explanation, debugging help, SQL generation, documentation drafts, regex, unit test suggestions, quick research, and boilerplate code. The appeal is obvious. Implementation cost is close to zero, the productivity gain is immediate, and no architectural change is required. Anyone with a browser tab can start today. The limitation is just as obvious once you look for it. The AI knows almost nothing about the surrounding system. Context, execution, verification, and integration all remain the human's job, along with every decision about what to do with the output. A useful test: what happens when the developer closes the conversation? If nothing else in the system changes, the organization is still at Stage 1, regardless of how many people are using the tool. Teams tend to move toward Stage 2 once AI becomes embedded in a repeatable workflow instead of being reached for opportunistically. Stage 2: Workflow Assistance At Stage 2, AI starts appearing inside processes that already exist rather than sitting off to the side. Think PR descriptions, code review preparation, test case generation, ticket classification, backlog refinement, release notes, documentation updates, and log or bug report summarisation. The shift here is subtle but important: AI now has context about a recurring task, not just a one-off question. It still generally produces something for a human to inspect before it's acted on, which is worth naming explicitly. I think of it as human-in-the-loop by default. A pattern I have seen repeatedly in delivery work illustrates this well. A team receiving detailed support requests would normally have someone read each one and manually work out the product area, severity, technical component, and the engineer it should go to. At Stage 2, AI produces that initial classification, and a person confirms or corrects it before it moves forward. That's meaningfully more mature than ad hoc chatbot use, but it's still far from autonomous operation. Nothing acts on the AI's output without a human saying yes first. Stage 3: Integrated Automation This is where things get architecturally interesting, and where technical depth starts to matter more than prompting skill. AI stops sitting beside the workflow and becomes connected to it, through APIs, webhooks, databases, CMSs, CRMs, ticketing systems, knowledge bases, ecommerce platforms and CI/CD pipelines. I will use an ecommerce example because it's one I know well from client work. Imagine a WooCommerce store gets a customer query about an order. At Stage 1, someone copies the question into an AI assistant and pastes in whatever context they happen to have on hand. At Stage 3, the system securely pulls the order information, shipping status, product details, customer history, and the relevant policy on its own, and uses that to draft an appropriate response. Nobody is manually assembling context anymore. In one subscription-based ecommerce implementation I worked on, this kind of integration was the difference between AI being a nice writing aid and AI genuinely shortening the support queue, and the same principle holds for the WordPress platforms we build at Web44: the value shows up once AI is wired into the CMS, not before. Integration cuts both ways, though. Once AI has system access, teams need to think seriously about permissions, authentication, data boundaries, logging, error handling, and fallback behavior. This is the point where AI architecture starts to matter more than prompt engineering, and where a lot of AI initiatives quietly stall. Stage 4: Agentic Operations Stage 4 is where AI generating an output turns into AI achieving an outcome, and it's worth pausing on that distinction because the two get conflated constantly. An agent doesn't just answer a question. It can inspect a problem, choose which tools to use, retrieve information, make intermediate decisions, carry out several actions in sequence, check whether the outcome worked, and escalate when it didn't. A concrete example: instead of simply telling an engineer that a deployment failed, an agent might inspect monitoring data, pull the relevant logs, compare the failure against known incidents, identify the likely service, open an issue with the right metadata, suggest a rollback, and then wait for human approval before doing anything irreversible. Here's the point I think matters most in this whole piece: agentic doesn't mean uncontrolled. If anything, increasing AI autonomy should increase governance, not reduce it. That means designing permission scopes, action boundaries, confidence thresholds, approval gates, execution limits, tool restrictions, and fallback paths before an agent touches anything that matters. What changes at Stage 4 isn't just capability; it's what the organization is willing to delegate. You're no longer only delegating generation. You're delegating decisions and actions, and that requires a level of trust in the system that has to be engineered rather than assumed. Stage 5: Governed AI-Native Delivery Stage 5 is where the real differentiation happens, and it isn't about giving AI more autonomy. It's about control. At this stage, AI has become part of the operational architecture, and the organization can answer a specific set of questions on demand: what did the AI do, why was it allowed to do it, what data did it access, did the action succeed, how much did it cost, was human intervention required, and can the action be reversed? This is where observability, audit logs, evaluation systems, reliability metrics, human escalation paths, cost monitoring, access controls, rollback mechanisms, security boundaries, and version management all become part of the day-to-day operating model rather than a one-off compliance exercise. In the AI governance work we do at AI44, I use a simple framework called TRACE: tiered autonomy, reversibility, access control, continuous assurance and explainability. It forces the question of whether an AI system's behavior can actually be explained and constrained before it's given more responsibility. The bigger idea underneath all of this: a mature AI system should make failure predictable. Software teams already build on the assumption that APIs fail, databases fail, and networks fail. AI should be treated the same way. LLM uncertainty needs to become an engineering constraint that gets designed around, not a surprise that shows up in production. How to Measure AI Maturity None of this is useful if it stays theoretical, so here are the metrics I would actually want an engineering leader to track. MetricWhat it tells usAI-assisted workflow percentageHow broadly AI participates in workFully integrated workflowsDepth of adoptionHuman intervention rateDegree of autonomyAI task success rateReliabilityException rateOperational weaknessTime saved per workflowProductivity impactCycle-time reductionDelivery impactCost per completed AI taskEconomic efficiencyRollback or error rateRiskAI incidentsOperational stabilityPercentage of actions requiring approvalGovernance and autonomy balance The underlying argument is simple. A company should be able to explain what AI changed operationally, not just how many people are using it. "Seventy percent of employees use AI weekly" is a nice statistic for a board slide. "AI reduced average issue triage time from four hours to twenty minutes while keeping the error rate under two percent" tells you something you can actually act on. Where you don't have real figures yet, treat these as hypothetical targets and say so plainly. The point isn't a specific number. It's having an honest, defensible answer when someone asks what AI is actually doing for the business. How Teams Should Progress Through the Stages One thing worth saying clearly: not every workflow should reach Stage 5, and that's what makes this framework useful rather than just aspirational. Brainstorming, architecture discussions, and exploratory research probably belong at Stage 1 or 2 permanently, and pushing them further would likely make the work worse, not better. Higher autonomy makes sense where the work is repetitive, actions are measurable, inputs are structured enough to reason about, errors can be detected, permissions can be tightly scoped, and outcomes can be reversed or escalated. Where those conditions aren't met, chasing more autonomy just adds risk without value. The goal was never maximum autonomy. It's appropriate autonomy, matched to the workflow, the stakes involved, and the organization's actual ability to monitor what's happening. Closing Thoughts The question worth asking your engineering organization is no longer whether your teams use AI. Most do, in some form, by now. The better questions are which workflows AI has actually entered, what decisions it's allowed to make, what actions it can perform on its own, how success is measured, and where human control deliberately remains. Organizations that can answer those clearly are the ones distinguishing real operational adoption from experimentation that never left the demo stage. AI maturity is not defined by how much autonomy an organization hands over to machines. It's defined by how deliberately that autonomy is designed.
If you are a data engineer, you have probably come across this situation where you have to decide between two approaches for refreshing enterprise data. One approach focuses more on speed and less on compute, while the other focuses more on reliability and data correctness. Usually, when we talk about data refresh, the two options that come to mind are incremental load and full load, and the question becomes which one we should choose. In my opinion, this is not necessarily a choice where we have to pick one approach over the other. Both approaches have advantages and disadvantages, and rather than relying on one approach, I believe we can combine them and use each where it makes the most sense. I learned this firsthand when I was tasked with building a data warehouse from scratch for my organization, and one of the most important decisions I had to make, apart from the data model and schema, was the refresh strategy. Why I Didn't Want to Pick Only One Approach Incremental loading is the modern approach and is commonly used in enterprise environments. Instead of loading the entire table every time the refresh runs, incremental loading focuses only on records added or modified since the previous refresh. This significantly reduces the data to process, resulting in faster refreshes and lower compute usage. However, incremental loading also has disadvantages, especially for historical data. Incremental loading assumes we can correctly identify all changes in the source system. For example, if a new record is added or an existing record is updated, we can generally identify those changes using a timestamp, watermark, or change data capture mechanism. The problem comes when a historical record is deleted from the source system. If our incremental process is only looking for new and updated records, then the deletion may never be captured. The source system may no longer have that record, but the data warehouse will continue to have it. This can result in the data warehouse slowly becoming different from the source system without necessarily causing the ETL process to fail. I consider this one of the biggest weaknesses of an incremental approach. The process can run successfully, but the data can still be incorrect. A full load, on the other hand, addresses this problem more simply. We can essentially reload the data from the source system and make sure that the warehouse reflects what is currently available in the source. This makes full load more reliable for historical changes and deletions. However, full load has its own problem. As the number of records increases from thousands to millions or even hundreds of millions, loading the entire table every time becomes expensive and time-consuming. A process that initially took a few minutes can eventually take hours, and running it every night may not be practical from a compute or maintenance-window perspective. So, in my opinion, neither approach is perfect. Where I Ran into This Problem Myself I was tasked with creating a data warehouse for my organization. When I investigated the existing environment, I found that the data I would be working with spanned millions of records across multiple tables, with a large number of columns. This is common when working with enterprise-level data, but it becomes an important consideration when designing a data warehouse and deciding how to refresh it. Prior to the launch of this data warehouse project, the data was stored in normalized tables coming directly from the ERP database. The data itself was good, but it was not very usable for a business intelligence developer or a data analyst. The main problem was that there was no one common place where an analyst could get all the information they needed. They had to work with multiple normalized tables and understand how those tables were related before they could even start building their analysis or dashboards. This was one of the main reasons the data warehouse project mattered. Once we kicked off the project, one important decision I had to make, besides the data model and schema, was the refresh strategy. I had two conventional options. The first option was incremental load, which is the modern approach where the primary focus is on faster refreshes and less compute. The second option was the traditional full load, where the primary focus is on reliability and data correctness, but it takes more time and compute because all the records are loaded every time the refresh runs instead of only loading the new and updated records. Now, in my opinion, both approaches have their own advantages and disadvantages. The modern approach is to employ incremental refresh, and this is what most enterprises prefer because of the faster refresh times and reduced compute. But I thought a little differently. Although incremental refresh has an advantage when it comes to speed and compute, the traditional full load also has some important advantages. It provides more reliability when it comes to data correctness, especially when historical records are deleted from the source system. Full load is also conceptually simpler because you do not have to build and maintain complex logic to identify every possible type of change that can happen in the source system. So instead of choosing between the two, I thought of employing a third approach. The Hybrid Approach The approach I decided to use was a hybrid approach, which meant combining incremental and full load refreshes instead of relying completely on either one. The idea was relatively simple. On weekdays, I used incremental refresh because business users and analysts are actively working with the data, and faster refreshes matter more. The incremental process would capture the latest and most frequently changing data without having to reload millions of records every time. Then, during the weekend, I would run a full load or reconciliation process to capture changes in historical data that the incremental process may not have been able to identify, especially deletions. This allowed me to achieve both objectives. I could have faster refreshes during the business days while still having a mechanism that periodically verifies that the data warehouse is aligned with the source system. In other words, instead of asking whether incremental or full load is better, I looked at where each approach was better and used both approaches accordingly. How the Hybrid Approach Works The approach can be generalized into a few steps. 1. Monday to Friday, Run Incremental Loads During weekdays (Monday to Friday), the ETL process retrieves only the records that have changed since the previous refresh. This can be done using Change Data Capture, a watermark column, or another reliable mechanism available in the source system. For example, if the source system has millions of records but only a few thousand records change during the day, there is no reason to process all the millions of records again. We can identify the changed records and process only those records. 2. Make the Process Idempotent Another important consideration is making sure that the ETL process can safely be executed more than once. For example, if an ETL job fails halfway through and we have to restart it, we should not end up inserting duplicate records into the data warehouse. This is where approaches such as MERGE-based upserts can be useful, although they need to be implemented carefully, especially in environments where multiple processes can update the same records concurrently. 3. Run a Full Reconciliation During the Weekend On Saturday or Sunday, when activity is generally lower, we can run a more comprehensive reconciliation against the source system. For smaller dimension tables, a complete truncate-and-reload may be perfectly reasonable. For very large fact tables, however, I would not necessarily recommend blindly truncating and reloading the entire table. Instead, we can compare the warehouse with the source and identify differences between the two. This is especially important for identifying historical records deleted from the source system. 4. Track What the Weekend Process Had to Correct I also think it is important not to treat the weekend reconciliation simply as another ETL process. Every time the full reconciliation finds a difference, that difference tells us something about our incremental process. For example, if the weekend process finds 100 deleted records that the incremental process did not capture, that is useful information. If that number continues increasing every week, it may indicate that there is a problem with the incremental logic or the way changes are being captured from the source. Therefore, the full load can also act as a validation mechanism for the incremental process. 5. Communicate the Refresh Window Finally, communicate when the full reconciliation will happen. If reports or dashboards are connected to the warehouse, users should know that the data may temporarily look different while the reconciliation or reload is happening. This should be treated as a planned maintenance activity rather than something unexpected. What This Approach Delivered Going into the project, I initially thought that I would have to accept one weakness or the other. Either I could use incremental refresh and get faster refreshes but accept that some historical changes, especially deletions, might not be captured. Or I could use full load and have better data correctness but accept longer refresh times and higher compute costs. The hybrid approach allowed me to avoid that trade-off every day. The warehouse could remain fast and relatively inexpensive to refresh during the business days, while the weekend reconciliation provided an additional layer of reliability and data correctness. Most importantly, historical changes that an incremental-only approach could potentially miss were given another opportunity to be captured during the weekend process. This made the refresh strategy more resilient because I was not relying on one mechanism to solve every possible problem. Where Would I Go From Here? In my opinion, the question should not be whether incremental load or full load is better. The better question is how we can use both approaches together to get the advantages of each. If your data warehouse currently relies completely on incremental refresh, you don't necessarily have to redesign the entire architecture. Start small. Take your highest-risk fact table and introduce a weekend reconciliation process alongside the existing incremental process. Then track how many records that reconciliation has to correct. That number can tell you a lot about the reliability of your incremental process. If the number is close to zero every week, that gives you confidence that your incremental process is working well. If the number keeps increasing, you have identified an area where the incremental logic needs improvement. For me, this is what made the hybrid approach interesting. It was not simply about making the data warehouse faster or cheaper. It was about finding a practical balance between performance, compute, reliability, and data correctness. Rather than choosing one approach and accepting its weaknesses, I chose to use each approach for what it was actually good at.
Part 1 of this series walked through why AI has compressed the gap between a vulnerability's disclosure and its exploitation from months to hours, laid out the lifecycle every vulnerability goes through, and mapped Project Glasswing, OpenAI Daybreak, Project Lightwell, Big Sleep, and CodeMender onto that lifecycle. The short version: discovery has gotten fast. Deciding what matters, fixing safely, and governing the automation doing the work have not caught up. This second part covers that second half, the part most vendor announcements leave out. The Layer Most Announcements Skip: Context The three programs above, and the tools around them, mostly strengthen discovery and fixing. What most enterprises actually lack is not more findings. It is useful context about the findings they already have. A vulnerability score alone does not say whether the affected component is deployed, reachable from the internet, connected to sensitive data, or sitting in a business-critical service. CISA's four-question test, built directly into Binding Operational Directive 26-04, is a practical version of this same idea applied to federal patch deadlines. Vulnerability flowchart A critical issue in a public API that handles customer records is not equivalent to the same CVE in an unused internal test library. Security programs are more effective when their workflows reflect that difference, combining technical severity with runtime reachability, exposure, privilege, and business impact rather than sorting a queue by CVSS score alone. This is exactly the argument I made in Graph Engineering: The Layer After Loop Engineering. Enterprise systems are defined by relationships: identities to permissions, permissions to cloud resources, resources to data, services to code, code to dependencies. Security risk lives in those same relationships, which is why an asset graph or attack-path view often does more for prioritization than another scanner. Mapping Everything to the Lifecycle Putting programs, tools, and supporting frameworks on the lifecycle diagram makes coverage and gaps easier to see. Lifecycle diagram – tools, gaps, etc. The gap is visible immediately. Nothing here covers "know what you run." No program can protect a software list that does not exist, which is why that step has to be handled internally, using SBOMs and dependency inventories, before any of the tools above add real value. Supporting work around the big three programs targets a second-order problem: faster discovery means more reports than open source maintainers can review. Anthropic has partnered with the Open Source Security Foundation's Alpha-Omega project to help maintainers triage incoming reports, and OpenAI's Daybreak commitment includes direct funding for groups such as Trail of Bits. Palo Alto Networks has also worked with IBM and Red Hat so its network-level protections can block attacks while a Lightwell fix is tested and rolled out, a practice known as virtual patching. It buys time. It does not replace the fix. AI Agents Are Identities Now, Not Just Tools There is a part of this system that most vendor comparisons leave out entirely: the AI agents doing the work are themselves a new attack surface that needs governance. An agent that reads source code, opens a pull request, queries a knowledge base, or changes a cloud setting is acting with real permissions. It should be treated the same way a service account or a contractor's login is treated, not as a feature toggled on inside a chat window. Agent governance Every production agent should have a unique identity rather than a shared credential. Permissions should be scoped to the task. Tool access should be explicit. Sensitive data should be classified before it enters a prompt or a retrieval pipeline. High-impact actions, like deploying a patch or changing an IAM policy, should require a human approval step, which is exactly the control that DeepMind applies to CodeMender's own patches. A mature security program should be able to answer, at any time: what did the agent access, what tools did it call, what changed, which policy allowed the action, who approved it, and can the agent's access be revoked immediately if something goes wrong. I wrote about the underlying discipline this requires in Production-Grade AI Agents and in From Prompts to Harnesses: How AI Engineering Has Grown Up. Memory boundaries, tool controls, guardrails, and observability are not optional extras for a chatbot. In a security context, they are exactly the controls that keep an autonomous remediation agent from becoming its own incident. The Rulebooks That Decide What Gets Fixed First Tools tell you what is possible. Frameworks tell you what to prioritize and how to prove you did it. FRAMEWORKPLAIN DESCRIPTIONWHERE IT FITSSOURCECISA BOD 26-04Four-factor risk test, three-day fix for the highest-risk vulnerabilitiesDecidecisa.govKEV catalogBugs known to be exploited right nowDecideCISA KEV catalogCVSSSeverity score for a bugDecide—EPSSLikelihood a bug will be exploitedDecide—SBOMA list of every component in your softwareKnow—VEXA note on whether a listed bug actually affects your productKnow, Decide—SLSAA trust scale for your build processKnow, Fix—OpenSSF ScorecardHealth check for open source dependenciesKnow—OWASP Top 10 for LLM applicationsCommon risks in AI-powered applicationsDefend the AI itself— The frameworks without a specific source link above are widely documented industry standards rather than a single paper or announcement; readers can find the current OWASP, CVSS, and SLSA specifications through their respective maintaining bodies (OWASP Foundation, FIRST.org, and the SLSA project) rather than a single citable report. A Practical Layered View Combining Part 1's tools and this part's context and governance layers gives a structure any team can use to check its own coverage. Context and governance layers LayerSupported by1. Know what you runSBOM, VEX, SLSA, OpenSSF Scorecard2. Find weaknessesGlasswing, Daybreak, Big Sleep, Codex Security, Claude Security3. Decide what mattersKEV, EPSS, CISA four-question test, attack-path graphs4. Fix safelyCodeMender, Lightwell, human code review5. Cover the gapVirtual patching, network segmentation6. Prove and learnIncident records, metrics, post-incident review7. Govern the AI agents doing the workAgent identity, least privilege, approval gates, audit logs Layer 1 remains the most commonly skipped step, and the one no vendor tool can fix for you. My earlier piece on a private, segmented network architecture using Terraform and Ansible is one example of reducing exposure at Layer 5 before a bug is even found: fewer exposed entry points means fewer urgent fixes later. Proving a fix actually worked, at Layer 6, depends on the same operational discipline covered in Incident Management and the Rise of AI SRE Agents and How AI Is Actually Changing SRE Tools, Part 2. Security and SRE teams increasingly share telemetry, runbooks, and the same requirement that automation be observable and reversible. Measure What Actually Matters Counting findings or closed tickets is not the same as reducing risk. More findings can mean better visibility. Fewer tickets can mean issues were closed administratively rather than actually fixed. MetricWhat it showsTime from discovery to verified remediationWhether the loop is fast and effectiveAge of critical internet-facing exposuresWhether the highest risk issues are addressed promptlyPercentage of critical findings with clear ownershipWhether accountability existsRecurrence rate for the same issue classWhether root causes are actually being removedPercentage of agent actions that are logged and reviewableWhether AI automation is governedTime to revoke an agent's credentials or stop an automated workflowWhether incident response is actually readyPercentage of releases with SBOMs and signed artifactsWhether supply chain controls are operating The question behind every metric is the same one CISA is now asking by regulation: are you reducing real risk faster, without creating a new source of operational risk in the process? Where to Start You do not need an invitation to any of the programs from Part 1 to begin. TimeframeFocusActionsFirst 30 daysKnow and measureBuild an SBOM for top applications, assign owners, measure the current gap between fix availability and fix deploymentDays 31 to 60Sort and prioritizeAdopt the CISA four-question test, add KEV and EPSS scoring to the queue, identify the single slowest fix in the environmentDays 61 to 90Add tooling and governanceTrial a code scanning or patching tool, evaluate a Lightwell-style remediation service or virtual patching for the hardest upgrade, and set identity, approval, and logging rules for any AI agent given write access to code or infrastructure Questions Worth Asking Any Vendor QuestionReasonWhich lifecycle step do you cover, and which do you leave to me?No single tool covers the full lifecycleWho confirms a finding is real before my team spends time on it?Unverified reports waste effortWho tests and signs the fix, and who is responsible if it breaks something?Fixes carry their own riskDoes your agent operate under a unique identity with logged, revocable access?Agents are identities, not just featuresWhat data leaves my environment, and is it retained?Access terms vary by program and are not always zero retention by defaultWhat protects me while the fix is being built?Coverage during the wait matters as much as the fix itself Conclusion None of the programs covered in this series replace a basic vulnerability management program. Glasswing, Daybreak, and Big Sleep speed up discovery. CodeMender and Lightwell speed up safe delivery of fixes. CISA's directive and the supporting frameworks tell you what to fix first. Virtual patching covers the gap while you wait. Agent identity and governance keep the automation itself from becoming a new risk. The strongest organizations will not be the ones that adopt the most tools from Part 1. They will be the ones that connect discovery, context, prioritization, remediation, governance and learning into one repeatable loop, and that treat their AI agents with the same identity discipline as any other privileged account. Start by finding the weakest link in that loop. If you cannot see your assets, fix discovery. If you cannot tell which findings matter, fix context. If your backlog keeps growing faster than you can clear it, fix remediation capacity. If an AI agent can act without clear boundaries, fix governance before you give it more access. Further Reading Graph Engineering: The Layer After Loop EngineeringProduction-Grade AI AgentsFrom Prompts to Harnesses: How AI Engineering Has Grown UpIncident Management and the Rise of AI SRE AgentsHow AI Is Actually Changing SRE Tools, Part 2Private VPC Network: Terraform and Ansible automation
I've spent years fixing broken Glue jobs and Lake Formation permissions as an AWS support engineer, then moved to pre-sales architecture for banking data platforms. One pattern shows up in almost every migration: teams build IAM policies and a data catalog as two separate workstreams, run by two separate teams, that never talk to each other. Then six months post-launch, someone asks, "Who can see the SSN column in the gold layer?" and nobody can answer without a week of manual auditing. That gap isn't a process problem. It's an architecture problem. This article walks through why classification and access control need to be the same system, with the actual policy constructs to make that work on AWS, and where teams get this wrong. The Failure Mode A typical lakehouse migration goes like this: Data engineering builds the pipeline: source → Bronze → Silver → Gold.A separate governance initiative (often 6-12 months behind) builds a catalog: Collibra, DataHub, Purview, whatever.Security writes IAM policies against S3 prefixes and Glue databases — because that's what exists at go-live. The catalog knows column cust_ssn is PII. IAM doesn't. So access control is granted at the table or prefix level, which means anyone with table access sees every column, including the ones the catalog explicitly flagged as sensitive. The classification work becomes documentation instead of enforcement. This is exactly the gap that tag-based access control was built to close, and most teams don't use it because it requires governance and security to share a data model, which is an org problem more than a technical one. Tag-Based Access Control: The Actual Mechanism On AWS, Lake Formation's LF-Tags let you attach classification metadata directly to catalog resources — databases, tables, even columns — and write IAM-style policies against the tags instead of the ARNs. Tagging a column as sensitive: Shell aws lakeformation add-lf-tags-to-resource \ --resource '{ "TableWithColumns": { "DatabaseName": "credit_risk_gold", "Name": "customer_360", "ColumnNames": ["cust_ssn", "cust_dob"] } }' \ --lf-tags '[{"TagKey": "data_classification", "TagValues": ["pii_restricted"]}]' Granting access by tag, not by table name: Shell aws lakeformation grant-permissions \ --principal '{"DataLakePrincipalIdentifier": "arn:aws:iam::111122223333:role/RiskAnalystRole"}' \ --permissions "SELECT" \ --resource '{ "LFTagPolicy": { "ResourceType": "TABLE", "Expression": [ {"TagKey": "data_classification", "TagValues": ["public", "internal"]} ] } }' The RiskAnalystRole above gets SELECT on anything tagged public or internal — automatically, across every current and future table with that tag. It gets nothing on pii_restricted columns unless a separate grant explicitly adds it. New tables inherit policy from their tags at creation time, not from a security team manually reviewing each new dataset. Databricks Unity Catalog does the equivalent with attribute-based access control and dynamic views: SQL CREATE VIEW customer_360_masked AS SELECT cust_id, CASE WHEN is_account_group_member('risk_pii_readers') THEN cust_ssn ELSE 'REDACTED' END AS cust_ssn, cust_dob, account_balance FROM customer_360_raw; Same principle: the masking rule reads from group membership, which is governed centrally, not hardcoded per consumer. Where This Actually Breaks in Practice Three failure points I've seen repeatedly on migration engagements: Classification lag. Auto-classification tools (Macie, Purview's classifiers) run periodically, not on write. A new column with SSNs can sit unclassified — and therefore unprotected by tag policy — for hours or days. Mitigation: enforce classification as a CI/CD gate on schema changes, not a background scan. Derived data breaks lineage. A gold-layer aggregate table built from a PII source doesn't automatically inherit the source's tags unless your ETL explicitly propagates them. I've seen teams tag Bronze meticulously and leave Gold completely untagged because "it's aggregated, so it's safe" — until someone adds a customer_email passthrough column six months later during a "quick fix." Vector stores are an ungoverned blind spot. RAG pipelines embed source documents into vector DBs. The embeddings are technically an obfuscation, not encryption, and can leak source content back out under adversarial querying. If your governance program stops at the relational catalog, your embedding store has none of these controls at all — this is the fastest-growing gap I see on client engagements right now. What to Actually Do About It If you're an architect on a migration, the ordering matters more than the tooling: Classification runs before or during pipeline design, not after go-live. Build it into the migration assessment.One tag taxonomy, enforced everywhere — Bronze, Silver, Gold, and vector stores alike. If your classification schema doesn't apply to your RAG pipeline, it's incomplete.Policy grants reference tags, never table/prefix names. This is the difference between a policy that scales and one that needs manual review for every new table.Audit logs (CloudTrail, Unity Catalog audit, Lake Formation permissions history) get mapped to your actual compliance controls (SR 11-7, GDPR Article 30, DPDP) as a standing report, not a one-time audit exercise. None of this is exotic. It's mostly discipline about doing classification first and wiring access policy to read from it, instead of treating them as two projects run by two teams on two timelines. References AWS Documentation – Lake Formation Tag-Based Access ControlDatabricks Documentation – Unity Catalog attribute-based access control and dynamic viewsNIST SP 800-53 Rev. 5 – Security and Privacy Controls for Information SystemsFederal Reserve SR 11-7 – Guidance on Model Risk ManagementDAMA International – DMBOK2
A confession first: I have spent the last few months building things that require actual thought. Turicum is a programming language. Perag is a personal RAG system that reads your documents so your AI doesn't have to. mdship manipulates Markdown files with the seriousness of a tool that knows people will build CI pipelines on top of it. All good, all real, all serious work with the hope that some will use them and make value. Sometimes you just want a brown bag project. The kind you can start, finish, and ship before your lunch sandwich gets soggy. So I built babbler. The Problem: Humans are Bad at Remembering Numbers With 9 Digits and No Personality Quick, what's your tax file reference number? Not the one you can look up. The one you can recall, right now, from memory, while a clerk on the phone is waiting and your insurance is somehow involved. Modern life hands you an endless parade of numbers exactly like this: Your health insurance claim is CLM-88213047.Your tax office reference is 2026/AT/0041938.Your car insurance policy is POL-773190-B.The ticket you opened with customer support, which you will need to reference again in three weeks, is #40921177. None of these are memorable. None of them are meant to be memorable. They're auto-increment counters or UUID fragments that some database handed out. The human is expected to somehow carry that around in the head or, more realistically, in a purse full of paper never to find. Nobody has ever said "oh yes, claim eight-eight-two-one-three-oh-four-seven, of course" out loud without sounding like they're reading a hostage's proof-of-life number. What humans are good at remembering are names. So: what if instead of 88213047, your claim was called Talo Buno? Instead of 88213048, it's Patu Luta. Two little pronounceable words instead of eight forgettable digits. You'd still write the number down somewhere official, but you could talk about "the Talo Buno claim" the way you'd talk about a pet, and actually remember which one it was. That's it. That's the whole idea. It's not going to save the world. It might save you one mildly awkward phone call. The Algorithm (Short Version, I Promise) The core idea: every counter-number gets converted into two pronounceable syllable-pairs, like Talo Buno or Patu Luta. Always four syllables, always two capitalized words, always the same result for the same number. Under the hood, it's mercifully simple — no cryptography, no hashing, no network calls, no "AI" (yes, I checked myself at the door on that one): Take 36 consonant-vowel syllables (ba, bo, bu, da... up to zu) — no h, no confusing look-alikes, just clean sounds that work across languages.Combine them two at a time to build words, and quietly remove the ones you wouldn't want on an official document (more on that in a second).Run the input number through a small, fixed bit of math — a bijection, meaning every number maps to exactly one name and every valid name maps back to exactly one number. No two different claim numbers ever accidentally get the same nickname. No retries, no collisions, no "sorry, try again" — just guaranteed-unique output, by construction, every time.Format the result as two capitalized words and hand it back. I went through a "hash it with MD5 and hope for the best" version first, because that's the obvious first instinct. Turns out "hope for the best" produced actual name collisions for about a third of all numbers, which is a spectacular way to guarantee two different insurance claims share a name. The fixed version replaced "hope" with actual math, which is generally a good trade. The MD5 version never even made it into a commit — some mistakes are too brief to leave fossils. The Blacklist (The Fun Part) Here's the part of this project that made me grin more than a numbering scheme has any right to: since the syllables get combined somewhat randomly, you occasionally get combinations you do not want stamped on someone's official tax document. So there's a small blacklist of forbidden two-syllable words the generator is guaranteed to never produce — permanently filtered out before any number is ever assigned, not caught after the fact. The list is a delightful little multilingual minefield: Some are baby-talk for things nobody needs on a government form, one is a slang word for police (and also, unrelated, poop, in Spanish), one means "senile," a couple is just international slang for excrement in three different languages, and two are, let's say, anatomically specific in a way that would not go over well in a customer support ticket. All tastefully avoided. Meanwhile, — and this delighted me — Mama, Papa, and Nana all made it through completely clean. Sometimes the universe hands you a nice coincidence. Try It Yourself Plain Text 10000 → "Talo Buno" 10001 → "Patu Luta" Every number from 0 up to about 1.64 million gets its own guaranteed-unique, guaranteed-(major EU languages plus Hungarian)inoffensive two-word name. One-way only — it makes names, it doesn't decode them back, because the actual point was never secrecy or reversibility, it was just "give me a name I can remember instead of a number I can't." MIT Licensed, Available in Java, Go, and Python The code — encoder, tests, docs, the works — is MIT-licensed, so grab it and use it however you like: https://github.com/verhas/babbler. It ships with matching, fully tested implementations in Java, Go, and Python — pick your poison; they all produce byte-identical names for the same number. And by "Java, Go, or Python," I obviously also mean any other language you like, because with three clean, well-documented reference implementations sitting right there, any reasonably competent LLM today can knock out a Rust, Kotlin, or — sure, why not — COBOL port before you've finished reading this sentence twice. Consider that a stretch goal I'm delegating to the machines. Brown bag project, delivered. Back to the serious stuff after the weekend. Disclosure: This article and its visuals were created with LLM assistance, but every claim, argument, and technical detail was thoroughly reviewed, fact-checked, and approved by the author before publication. No "slop" here. To be clear about what this means: I used an AI as a tool for composition and iteration—much like using a typewriter instead of painstakingly handwriting each page with a goose feather, let alone carving it into stone. The tool speeds up the mechanical work; it doesn't replace the thinking. The tone, the structure, the emphasis — all human judgment. This is how AI tools should be used in knowledge work: as amplifiers of clarity and productivity, not as replacements for expertise or accountability. A human — specifically, I stand behind every word.
Azure Key Vault is the well-behaved citizen of the Azure secrets world. Every secret is versioned; you get soft-delete and purge protection; access is governed by RBAC or access policies; and DefaultAzureCredential means your app authenticates the same way locally (az login) and in production (Managed Identity/Workload Identity on AKS) with zero code changes. And yet the same old gap is there. Your Go service calls GetSecret once at startup, decodes the value into a struct, and treats it as immutable for the life of the process. When someone rotates the secret in Key Vault — a new version, the old one disabled — your service has no idea. It keeps using the value it read at boot until the next deploy or pod restart. The rotation was seamless in Azure and invisible to your app, which is exactly the wrong combination. mamori is a Go library I built to fix this class of problem across every backend. Point it at a Key Vault secret, and it watches for new versions, pulls the change, re-validates your whole config, and swaps it in atomically — while your service keeps running. This is the Azure entry in the mamori launch series. For the full model, start with Introducing mamori: Typed, Watchable Config & Secrets for Go; here we focus on Key Vault. The azure-kv Scheme The providers/azure module registers the azure-kv scheme. The grammar is azure-kv://<vault-name>/<secret-name>[#json-key][?version=<v>]: Plain Text azure-kv://my-vault/db-password # latest enabled version azure-kv://my-vault/api-key?version=abc123 # pin to a specific version id azure-kv://my-vault/creds#password # one key out of a JSON secret One Key Vault quirk to keep in mind: versions are identified by opaque hex IDs, not the sequential 1, 2, 3 you get in GCP Secret Manager. So ?version= here pins to an id like abc123def456..., not a number. Leave ?version off and mamori tracks whatever the latest enabled version resolves to — which is what you want for credentials that rotate. Setup Shell go get github.com/xavidop/mamori go get github.com/xavidop/mamori/providers/azure Register it with a blank import. It authenticates through DefaultAzureCredential, so it picks up your az login locally, a Managed Identity on a VM/App Service, or Workload Identity on AKS — automatically, in that order: Go import _ "github.com/xavidop/mamori/providers/azure" The identity you bind needs read access to the vault's secrets. With Key Vault's RBAC model, that's the built-in Key Vault Secrets User role, scoped to the vault (or to individual secrets): Shell az role assignment create \ --role "Key Vault Secrets User" \ --assignee "<managed-identity-object-id>" \ --scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/my-vault" If your vault still uses the classic access-policy model instead of RBAC, grant Get (and List) on secrets. Load, Then Watch the Rotation Go package main import ( "context" "log" "time" "github.com/xavidop/mamori" "github.com/xavidop/mamori/secret" _ "github.com/xavidop/mamori/providers/azure" ) type Config struct { // Tracks the latest enabled version: rotations flow in automatically. DBPassword secret.String `source:"azure-kv://my-vault/db-password"` // Pinned to an immutable version id: deliberate, reproducible. SigningKey secret.String `source:"azure-kv://my-vault/signing-key?version=abc123"` // One field out of a JSON secret. APIToken secret.String `source:"azure-kv://my-vault/creds#api_token"` } func main() { ctx := context.Background() w, err := mamori.Watch[Config](ctx, mamori.WithPollInterval(60*time.Second), mamori.OnChange(func(ev mamori.Change[Config]) { if ev.Changed("DBPassword") { pool.Rotate(ev.New.DBPassword.Reveal()) // live, no restart log.Println("Key Vault: db-password rotated and applied") } }), mamori.OnError(func(err error) { metrics.Inc("config_error") }), ) if err != nil { log.Fatal(err) } defer w.Close() cfg := w.Get() // last VALID config, lock-free _ = cfg } Every secret field is a secret.String, so it redacts itself in fmt, slog, and JSON — a stray log.Printf("%+v", cfg) prints [REDACTED] — and only reveals plaintext through an explicit .Reveal(). mamori also ships a go vet analyzer that fails your build if an azure-kv:// value lands in a plain string instead of a secret.String. What Reconciliation Guarantees When a new version of db-password becomes the latest enabled one, mamori's poll cycle detects the version change, fetches it, and then: Re-validates the entire struct – a value that fails a validate rule is rejected atomically; Get() keeps returning the last good config and OnError receives a *ValidationError. A bad secret can't break your service.Swaps atomically – w.Get() moves from old to new in one lock-free store; no reader ever sees a half-updated config.Fires a diff-aware callback – ev.Fields tells you exactly which fields moved, so you only rebuild what changed. Key Vault has no push channel for "a new version was created", so mamori polls — politely: the interval is jittered by ±20% (tunable with WithJitter) so a fleet of pods doesn't hammer the vault on the same tick, and change detection compares the version id before pulling the payload, so an unchanged secret is cheap. If Key Vault is briefly unreachable, mamori retries with backoff and keeps serving the last good value; wire WithStale(10*time.Minute) and check for a *mamori.StaleError in OnError if you want to page only on a prolonged outage rather than a transient blip. Structured Secrets With flatten Storing a whole connection blob as one JSON secret? Decode it straight into a nested struct: Go type Config struct { DB DBConfig `source:"azure-kv://my-vault/db-conn" flatten:"json"` } type DBConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Password secret.String `mapstructure:"password"` } One Key Vault call, one JSON document, cleanly typed — with the password still redacting itself. When You Don't Need This If a secret only ever changes on deploy, a plain mamori.Load at boot is all you need — no watching, no poll loop. mamori earns its keep the moment a Key Vault secret rotates while your process is running and you'd rather rebuild a client than recycle a pod. Wrapping Up Azure Key Vault rotates your secrets cleanly; mamori makes your Go service actually notice. It watches Key Vault, reconciles a new version into your running process — typed, validated, redacted — and lets you pin the versions you want frozen. Shell go get github.com/xavidop/mamori/providers/azure Full introduction: Introducing mamoriDocs: mamorigo.dev/docsSource: github.com/xavidop/mamori mamori is MIT-licensed and at v0.1.0 — early and moving fast, so issues and feedback are very welcome. Happy coding!
API testing is the practice of verifying that an API behaves as expected. In Agile environments, teams often perform it early in the SDLC as part of shift-left testing, enabling faster feedback and helping them identify and fix defects early. Playwright is a modern test automation framework that’s popular among testers and developers. Built by Microsoft, it makes it easy to automate both web and API testing across multiple programming languages, all within a single tool. In this tutorial, we will focus on testing APIs using Playwright TypeScript. By the end of this article, you should have a clear understanding of writing automation tests for PUT, PATCH, and DELETE API requests with Playwright TypeScript. Check out the previous articles to learn how to test GET API request and verify response data with Playwright TypeScript. What are PUT Requests? PUT requests are commonly used to update an existing resource by replacing its current data with the data provided in the request body. They are similar to POST requests; the Content-Type header plays an important role because it tells the server how to process the data being sent. When a PUT request is successful, it typically returns a 200 status along with the updated resource in the response. However, this can vary by API design; some APIs may return only a status code without a response body. How to Test PUT API Requests With Playwright TypeScript Playwright offers a request API that lets us create and manage HTTP request contexts easily. We’ll use the PUT /updateOrder API that updates the available order using its order ID. This API is a part of the RESTful e-commerce application available on GitHub. This API requires an authentication token to validate the request and authorize the update; if the token is missing or invalid, the request will fail. The order ID needs to be passed as a path parameter so the correct order can be identified, and the updated details should be included in the request body in JSON format. Since this is a PUT request, you must send the entire order object, not just the field being modified. Even for a small change, the full set of order data is required. We’ll use the following test scenario to demonstrate testing the PUT API request with Playwright TypeScript: Markdown ## Test Scenario Title: Update the existing orders in the system. ### Pre-condition: Valid orders are available in the system #### Test 1. Update all the order details of order_id "1" 2. Verify that the Status Code 200 is returned in the response 3. Assert that the order details have been updated correctly To implement this test scenario, we’ll need to perform the following steps: Hit the Auth API and extract the token.Write a test to send the PUT API request and pass the token extracted in step 1. Step 1: Create a Fixture to Generate and Extract the Token We’ll need the auth token for PUT, PATCH, and DELETE API requests, so let’s create a fixture that hits the POST /auth API and extracts the token for use across other tests. Let’s create a new fixture file auth.fixture.ts, in the fixtures folder in the project’s root directory. TypeScript import { test as base, expect } from "@playwright/test"; import dotenv from "dotenv"; dotenv.config(); type MyFixtures = { token: string; }; export const test = base.extend<MyFixtures>({ token: async ({ request }, use) => { const response = await request.post("http://localhost:3004/auth", { data: JSON.stringify({ username: process.env.USERNAME, password: process.env.PASSWORD, }), headers: { "Content-Type": "application/json", }, }); expect(response.status()).toBe(201); const responseBody = await response.json(); await use(responseBody.token); }, }); export { expect } from '@playwright/test'; A custom Playwright test setup is created by extending the base test object and adding a reusable token fixture. It uses the dotenv library to load environment variables like USERNAME and PASSWORD, ensuring sensitive data is not hardcoded, and defines a TypeScript type to make the token available with proper type safety. A .env file is created containing the credentials and placed in the project’s root folder. This ensures best practices are followed and sensitive information, such as usernames and passwords, is not exposed. As this is a demo API, the credentials used with the POST /auth endpoint are provided below. In a real-time project, the .env file should be added to the .gitignore file to ensure it is not committed to the remote repository. Plain Text #.env USERNAME=admin PASSWORD=secretPass123 Inside the fixture, an authentication API is called to generate a token; the response is validated, and the token is extracted and passed to the tests using the use() function. Finally, expect is re-exported so that both test and expect can be imported from this single fixture file, keeping test imports clean and consistent. Step 2: Updating the Order With a PUT Request and Verifying the Response Let’s create a new spec file to update the order using the PUT /updateOrder API request. TypeScript # 5_put-test.spec.ts import { test, expect } from "../fixtures/auth.fixture.js"; test("Update Order using PUT API Request", async ({ request, token }) => { const updatedOrder = { user_id: "89", product_id: "124", product_name: "Lindt Choclate", product_amount: 50, qty: 3, tax_amt: 10, total_amt: 160, }; const id = 1; const response = await request.put( `http://localhost:3004/updateOrder/${id}`, { data: JSON.stringify(updatedOrder), headers: { "Content-Type": "application/json", Authorization: `${token}`, }, }, ); const responseBody = await response.json(); expect(response.status()).toBe(200); expect(responseBody).toEqual( expect.objectContaining({ message: "Order updated successfully!", order: expect.objectContaining({ product_id: "124", product_name: "Lindt Choclate", }), }), ); }); Code Walkthrough Using the auth fixture for getting the token: The test imports a custom test from the auth fixture, which automatically provides the token as part of the test context. This token is generated before the test runs and injected via Playwright fixtures, eliminating the need to manually authenticate in each test.Request body: The updatedOrder object represents the payload sent in the request to update an existing order. It contains the required fields for the order update request body. This object is later serialized into JSON format before being sent to the API.Sending the PUT request: The request.put() function is used to send an HTTP PUT request to update the order with a specific ID. The request includes the serialized request body and headers such as Content-Type and Authorization, where the token is passed for authentication. This step performs the actual API interaction to update the order in the system.Dynamic endpoint handling: The order ID is stored in a variable and dynamically appended to the API endpoint using template literals. This makes the test more maintainable and reusable.Verifying the response: The response is first converted to JSON, and the status code is validated to be 200, indicating a successful update. Then, assertions are made using expect.ObjectContaining that partially validates the response body, ensuring that the success message and updated order details match expected values. Test Execution Before running the test, the RESTful e-commerce application should be started using Docker by running the following command from the terminal: Plain Text docker compose -f docker-compose-ecommerce.yml up -d The following command should be run from the terminal: Plain Text npx playwright test tests/5_put-test.spec.ts The following test output shows that the test was executed successfully: What Are PATCH Requests? A PATCH request is used when we want to update only part of a resource, not the whole thing. It’s similar to a PUT request, but the main difference is that PUT expects the entire payload, whereas PATCH lets us send only the fields we want to change. How to Test PATCH API Requests With Playwright TypeScript We’ll use the PATCH /partialUpdateOrder API from the RESTful e-commerce application that updates the available order using its order ID. Similar to the PUT API request discussed in the earlier section of this article, the PATCH API also needs an authentication token to update the order. As it is a PATCH request, we don’t need to send the full payload. Only the fields that require updates should be included in the request body. Test Scenario: Markdown ## Test Scenario Title: Partially update an existing orders in the system. ### Pre-condition: Valid orders are available in the system #### Test 1. Update the user_id and product_id for the order ID - 1 2. Verify that the Status Code 200 is returned in the response. 3. Assert that the order details have been updated correctly. The PATCH API request requires an authorization token. We’ll use the token from the auth.fixture.ts file that we generated in the earlier section while discussing the PUT API request. Let’s create a new spec file to partially update an order using the PATCH /partialUpdateOrder API. TypeScript # 6_patch-test.spec.ts import { test, expect } from "../fixtures/auth.fixture.js"; test("Update Partial Order using PATCH API Request", async ({ request, token, }) => { const updatedOrder = { user_id: "98", product_id: "214", }; const id = 1; const response = await request.patch( `http://localhost:3004/partialUpdateOrder/${id}`, { data: JSON.stringify(updatedOrder), headers: { "Content-Type": "application/json", Authorization: `${token}`, }, }, ); const responseBody = await response.json(); expect(response.status()).toBe(200); expect(responseBody).toEqual( expect.objectContaining({ message: "Order updated successfully!", order: expect.objectContaining({ user_id: "98", product_id: "214", }), }), ); }); Code Walkthrough Using the auth fixture for getting the token: The test uses a custom fixture that automatically provides a token, so we don’t have to call the POST /auth API every time. The token is generated before test execution and injected into the test context, ensuring secure and reusable authentication across multiple tests.Request body: The updatedOrder object contains only the fields user_id and product_id that need to be modified. As it is a PATCH request, there is no need to send the complete order payload; this makes the request lightweight and focused on partial updates.Sending the PATCH request: The request.patch() function is used to send a partial update request to the API endpoint, with the order ID dynamically added to the URL. The request includes the JSON-stringified body along with the Content-Type and Authorization headers. The token is passed in the Authorization header.Verifying the response: The response is parsed into JSON and validated to ensure a 200 status code is returned after the PATCH API is hit successfully. This verification ensures that the operation was successful. Then, expect.objectContaining is used to validate the response body, confirming that the success message and updated fields match the expected values. Test Execution To run the test, the following command should be executed from the terminal: Plain Text npx playwright test tests/6_patch-test.spec.ts The following screenshot of the test execution confirms that the test was run successfully: What Are DELETE Requests? A DELETE API request is used for removing a specific resource from the server. In most cases, it doesn’t return a response body. The resource is identified using a URI, and once the request is processed, it’s permanently deleted. Because of this, DELETE operations can have side effects, like removing records from a database. A DELETE request should be handled carefully because: Data deleted through a DELETE request is permanently removed and cannot be restored.It directly impacts the server data; hence, it is not considered a safe method.It may lead to inconsistencies if not managed appropriately.DELETE isn’t always idempotent in real-world scenarios. For example, the first DELETE request might return a 204 (No Content) indicating success, while calling it again on the same resource could return 404 (Not Found) because the resource has already been removed. How to Test DELETE API Requests With Playwright TypeScript We’ll use the DELETE /deleteOrder API endpoint from the RESTful e-commerce application for this demonstration. This API requires an order_id as a path parameter to identify the order to delete. A valid authentication token must be included in the headers. On successful execution, the order is removed, and a 204 (No Content) status is returned. Want to learn more about API Testing? Check out this tutorial to get hands-on API Testing Test Scenario: Markdown ## Test Scenario Title: Delete existing order from the system. ### Precondition - There should be some valid orders available in the system. #### Test Steps: 1. Delete the order with order_id "6" using the DELETE request. 2. Verify that 204 Status Code is returned in the response. To implement this test scenario, we need valid orders in the system, which can be created using the POST /addOrder API. Refer to the earlier tutorial on testing POST requests with Playwright TypeScript to explore different ways to generate request payloads and test the POST API request. Test Implementation Let’s create a new spec file to delete an order using the DELETE /deleteOrder API. TypeScript import { test, expect } from "../fixtures/auth.fixture.js"; test("Delete an order using DELETE API Request", async ({ request, token }) => { const id = 6; const response = await request.delete( `http://localhost:3004/deleteOrder/${id}`, { headers: { Authorization: `${token}`, }, }, ); expect(response.status()).toBe(204); }); Code Walkthrough Using the auth fixture for getting the token: The test imports a custom test from the auth fixture, which automatically provides a valid authentication token. This token is generated before the test runs and injected into the test context, so there’s no need to call the POST /auth API explicitly before the DELETE API.Sending the DELETE request: The request.delete() function is used to send a DELETE request to the API endpoint, with the ID dynamically passed in the URL to identify the order to be deleted. The request includes the Authorization header, where the token is sent to authenticate the request. No request body is required for this operation.Assertion: The test verifies that the response status is 204, which indicates that the request was successfully processed and the resource was deleted. Since DELETE APIs often don’t return a response body, validating the status code is sufficient to confirm success. Test Execution To run the test, run the following command from the terminal: Plain Text npx playwright test tests/7_delete-test.spec.ts The following screenshot confirms that the test ran successfully: A simple, practical approach is to call the GET API after running a DELETE request to confirm the resource has been removed from the system. Summary Testing PUT, PATCH, and DELETE APIs with Playwright and TypeScript helps ensure that the application handles updates and deletions correctly. While challenges such as managing test data, handling authentication, and maintaining test independence can arise, Playwright’s fixtures and built-in request handling make it easier to create clean and reusable tests. The main advantage is the ability to validate end-to-end API behavior with clear assertions and strong TypeScript support. In my experience, the tests should be isolated, avoid unnecessary dependencies between them, and validate both status codes and response data to make the API tests more robust and maintainable. Happy testing!!