Moving 900+ GB of Prometheus Without a Cutover Weekend
Feature Engineering Techniques That Actually Improve Model Accuracy
Agentic AI Threat Intelligence Essentials
Getting Started With Agentic AI for SecOps
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.
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.
Somebody ships a differential privacy layer in front of the event store and leaves the dashboards sitting on top of it. The queries still return, the totals still look like totals, and the ticket gets closed. Nobody on the team computed the floor. Every private aggregate carries noise, and the noise is unlabeled. It sits in the same cell somebody reads on Monday morning before deciding whether a segment moved, and to the eye it looks like a small real change. In the ad-account work I do, the move to aggregate and private reporting comes from regulation rather than from architecture, so the debate never really happens, and the pattern is always the same: epsilon gets negotiated in a meeting, and the arithmetic that would turn it into a detection limit never makes it into any document. That conversion is four lines. The rest of this runs them against schema choices teams actually ship. Name the Unit Before Anything Else Differential privacy protects a unit, and you have to say which one. Everything below assumes user-level privacy: add or remove one user together with all of their events. That choice causes most of the confusion that shows up later. The textbook line, a histogram has sensitivity 1 no matter how many buckets it has, is an event-level statement. One row in, one row out. Under user-level privacy, a single user can drop forty events into one cell, and forty is your sensitivity. So one quantity runs through every formula below: the largest total contribution one user can make to one release, in the units of the metric. Events, for counting queries. It is a cap you write into the job. You do not get it from a MAX() over yesterday's users. Wilson and colleagues (2020) built exactly this into a production SQL engine, because a query with no bound has no calibrated noise to add. With that in hand, the Laplace mechanism is a one-liner: noise of scale b = bound / epsilon (Dwork, McSherry, Nissim and Smith, 2006), and a single noisy count with standard deviation b * sqrt(2). The Number That Belongs in a Schema Review The cell people argue about is almost always a difference, this week minus last week or treatment minus holdout or one region minus the next. The difference of two independent Laplace variables is not Gaussian, and the shortcut is off by enough to matter. For Z = Lap(b) - Lap(b) the exact two-sided tail is exp(-t/b) * (1 + t/(2b)). Set it to 0.05, and the critical value is 4.11b. The normal approximation gives 1.96 * 2b = 3.92b, an interval that in fact covers 94.1%. Reviewers do not catch the missing 5.9% of coverage, because the chart still has two lines on it. Schema review needs a different number anyway. Significance is the question you ask once a movement is already on the screen. Before the pipeline ships, what you need is the size of movement it can be expected to catch at all. At 80% power that comes out at 5.58b, taken from the exact law and then checked by simulation. Treating the Gaussian 3.92b as if it were the detection floor is the substitution that flatters the pipeline. Python import math, random def tail(u): # P(|Lap(b) - Lap(b)| > u*b) return math.exp(-u) * (1 + u / 2) def critical_value(alpha=0.05, lo=0.0, hi=60.0): for _ in range(200): mid = (lo + hi) / 2 lo, hi = (mid, hi) if tail(mid) > alpha else (lo, mid) return (lo + hi) / 2 # in units of b def schema_cost(epsilon, per_user_bound, releases): """What one release costs you in events, given a user-level bound.""" b = per_user_bound * releases / epsilon # basic sequential composition crit = critical_value() return b, b * math.sqrt(2), crit * b, 5.578 * b for label, bound, releases in [ ("one event per user, published once", 1, 1), ("bound 24 events per user, published once", 24, 1), ("bound 24, republished nightly for a month", 24, 30), ("bound 5, republished nightly for a month", 5, 30)]: b, sd, crit, mde = schema_cost(1.0, bound, releases) print(f"{label:44s} b={b:7.1f} sd={sd:8.1f} " f"significant above={crit:8.1f} 80% power needs={mde:9.1f}") At epsilon = 1.0: Plain Text one event per user, published once b= 1.0 sd= 1.4 significant above= 4.1 80% power needs= 5.6 bound 24 events per user, published once b= 24.0 sd= 33.9 significant above= 98.7 80% power needs= 133.9 bound 24, republished nightly for a month b= 720.0 sd= 1018.2 significant above= 2961.4 80% power needs= 4016.2 bound 5, republished nightly for a month b= 150.0 sd= 212.1 significant above= 617.0 80% power needs= 836.7 All four rows spend the same epsilon. On the first row, 80% power is six events. On the third, it is four thousand. The only things that moved are the per-user bound and the nightly refresh. The bound and the refresh get treated as one knob, and they do not move the same quantity. The bound is what you pay for, and coarsening does not lower it. Roll hourly buckets up into six-hour blocks, and one user's total contribution across the release is what it was, so b sits where it was. Coarsening still helps, only through the denominator: more events per cell means the same absolute noise is a smaller share of it. Releases accumulate. A nightly job over a rolling window is thirty releases in a month, and under basic composition each one gets epsilon / 30. Teams count dashboards and then spend the budget on refresh cadence, including tiles that never get opened. Flat Noise, Very Unflat Keys Noise is added at one scale for every cell. Real event mass is not, and the gap is where a private report stays readable on the top row and falls apart below it. Two measurements from a corpus I work with, roughly 24 million rows of ad-account data. In one product niche, a single three-word search query takes between 6.5% and 41.6% of an account's total spend, median near 17.7% across six competitors. Across seven product categories in the same corpus, the share of spend sitting in one country runs from 10.8% to 81.1%. Those two figures are spend shares, not event counts, and they describe spread across accounts rather than neighboring cells inside one report. I am not using them as a stand-in for cell mass. I use them for the shape: in data of this kind, one key routinely carries a double-digit percentage of everything, and the rest of the distribution drops off fast. What the simulation below reproduces is that shape; the corpus itself never enters the code. Python def laplace(rnd, b): u = rnd.random() - 0.5 return -b * math.copysign(1.0, u) * math.log(1.0 - 2.0 * abs(u)) def skew(total, keys, b, tol=0.10, reps=300, seed=7): rnd = random.Random(seed) w = [1.0 / i for i in range(1, keys + 1)] w = [x / sum(w) for x in w] # heavy head, long tail truth = [total * x for x in w] over_keys, over_mass = 0, 0.0 for _ in range(reps): rel = [abs(laplace(rnd, b)) / t for t in truth] idx = [i for i, r in enumerate(rel) if r > tol] over_keys += len(idx) over_mass += sum(w[i] for i in idx) return 100 * over_keys / (reps * keys), 100 * over_mass / reps for tag, b in [("bound 24, 30 releases (b=720)", 720.0), ("bound 5, 30 releases (b=150)", 150.0), ("bound 24, one release (b=24)", 24.0)]: pct_keys, pct_mass = skew(100_000, 720, b) print(f"{tag:32s} keys over 10% error={pct_keys:5.1f}% events in them={pct_mass:5.1f}%") Plain Text bound 24, 30 releases (b=720) keys over 10% error= 98.4% events in them= 74.9% bound 5, 30 releases (b=150) keys over 10% error= 93.9% events in them= 53.2% bound 24, one release (b=24) keys over 10% error= 76.0% events in them= 28.2% The top key holds 14% of all events and carries about 7% relative noise, so the headline stays healthy. The tenth-largest key is already past 50%. Rank 100 sits at 729%. Half the events live in the top twenty keys. Volume can look fine while almost every key in the same release is already unusable, and the chart does not flag which cells are which. Five Lines for the Schema Review Write the decision threshold next to the query, in events. If 5.58 * b is bigger than the difference that would change what you do, the query cannot answer it at this budget, and no amount of chart work fixes that.The per-user bound is a decision you make once and then live with. Tightening it from 24 to 5 brought that floor down by a factor of almost five in the table above. It also censors your heaviest users, so part of the variance you win comes back as bias. Measure how much you clip and report it beside the estimate.Resolution is bought by raising cell mass: coarser keys, longer windows, fewer dimensions per release. b stays where it was; what improves is the noise-to-mass ratio.Budget accrues per release, not per dashboard. Refreshes, backfills, and one-off reruns over the same users all spend it.A non-private control aggregate is for internal audit and stays there. Published next to the private number, the difference between the two is the noise, handed back in clear. The architecture choice sits underneath all five. Local differential privacy of the RAPPOR kind (Erlingsson, Pihur and Korolova, 2014) moves the noise onto the client, and for the same epsilon the floor lands much higher, with no amount of schema work bringing it back down. Shuffling designs such as Prochlo (Bittau and colleagues, 2017) change the trust model, and the arithmetic changes with it. Every number above assumes the central model, so a team that has not settled its trust boundaries cannot quote a floor to anyone. Where This Stops This is about measurement precision, not delivery. An aggregate can tell you an effect exists; it cannot restore per-user selection to a system that lost it. Small cells stay lost no matter what you set. The working rule is relative: if a cell's true count is not several times b * sqrt(2), fold it into a larger bucket instead of publishing a number you cannot defend. Which cells qualify depends on your bound and your release count, so the threshold is local to the pipeline. One thing I have not measured: how much of a causal estimate survives an end-to-end private stack, where on-device selection feeds an aggregated reporting API. I would like that number from anyone running it in production, specifically how wide the confidence interval on a lift estimate ends up at the far side. I have not seen it characterized in public, and it determines whether private measurement is good for decisions or only for reporting. It is an afternoon of work, and it has to happen before go-live. After that, nobody in the building can say which part of a movement was the noise. References Dwork, C., McSherry, F., Nissim, K., Smith, A. (2006). Calibrating Noise to Sensitivity in Private Data Analysis. Theory of Cryptography, LNCS, 265-284. https://doi.org/10.1007/11681878_14Wilson, R. J., Zhang, C. Y., Lam, W., Desfontaines, D., Simmons-Marengo, D., Gipson, B. (2020). Differentially Private SQL with Bounded User Contribution. Proceedings on Privacy Enhancing Technologies, 2020(2), 230-250. https://doi.org/10.2478/popets-2020-0025Erlingsson, U., Pihur, V., Korolova, A. (2014). RAPPOR: Randomized Aggregatable Privacy-Preserving Ordinal Response. ACM CCS 2014, 1054-1067. https://doi.org/10.1145/2660267.2660348Bittau, A., Erlingsson, U., Maniatis, P., Mironov, I., Raghunathan, A., Lie, D., Rudominer, M., Kode, U., Tinnes, J., Seefeld, B. (2017). Prochlo: Strong Privacy for Analytics in the Crowd. SOSP 2017, 441-459. https://doi.org/10.1145/3132747.3132769
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.
If you need to generate prerequisite test data, such as adding dummy records through an API, before executing your test suite, using Global Setup in Playwright is a clean, maintainable, and scalable approach. It ensures all required data is prepared upfront, keeping your test execution structured and reliable. What Is Global Setup? The Global Setup in Playwright runs once before the entire test suite begins execution. It is typically used for activities such as creating test data, seeding the database, generating authentication tokens, preparing the test environment, or cleaning up old data. Since it executes only once at the start of the run, it helps ensure efficient and consistent test initialization across the suite. How to Create a Global Setup in a Playwright TypeScript Project The following steps need to be followed to create a globalSetup in the Playwright project: Step 1: Create a global-setup.ts file in the Root Folder of the Project TypeScript import orders from './test_data/orders.json' with {type: 'json'}; async function globalSetup() { console.log('Running Global Setup...'); const apiContext = await request.newContext(); const response = await apiContext.post("http://localhost:3004/addOrder/", { data: orders, }); if(response.status() !=201) { throw new Error ("Failed to create setup test data!"); } console.log('Test data created successfully'); } export default globalSetup; The globalSetup() function will insert test data into the RESTful e-commerce demo application. The test data for creating new orders is supplied using the following orders.json file. JSON [ { "user_id": "1", "product_id": "79", "product_name": "5 star 10gm Chocobar", "product_amount": 5, "qty": 1, "tax_amt": 0.5, "total_amt": 5.5 } ] The data is required to validate the GET API tests, which will execute immediately after the Playwright test command is triggered. Step 2: Register globalSetup in playwright.config.ts TypeScript import { defineConfig } from '@playwright/test'; export default defineConfig({ globalSetup: './global-setup', }); The globalSetup property tells Playwright to execute the file once before the entire test suite runs. This setup file is typically used to perform pre-test activities such as creating test data or preparing the test environment, ensuring that all tests start with the required initial state. tsconfig.json: JSON { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, "types": ["node", "@playwright/test"] } } In this case, “require.resolve()” is not used because the project is configured to use ES Modules ("module": "NodeNext" and "moduleResolution": "NodeNext" in tsconfig.json). Since the project runs in ESM mode, Playwright can directly resolve the setup file using a string path ('./global-setup') instead of relying on CommonJS-based require.resolve(). Step 3: Run the Test The tests are already in place; only global-setup.ts needs to run to feed data into the system before running them. The script for running the tests is updated in the package.json file: JSON "scripts": { "test": "npx playwright test" }, Let’s run the tests by executing the following command in the terminal: Plain Text npm run test The following is a screenshot of the output after running the tests: It can be seen that the test data was created using the global setup before running the tests. How This Helps in Your API Testing Instead of creating the order in the first test and relying on test execution order, we can use a more robust approach by setting up the required data beforehand. By creating test data once during global setup, the tests remain independent, reducing flaky dependencies and unintended failures. This approach aligns with Playwright best practices and helps build a stable, maintainable automation test suite. Important Points to Note About the Global Setup It runs once per test run.It does NOT share variables directly with tests.Use files or environment variables to pass data.Assertions should not be put in the global setup, except for setup validation. Check out how to verify response data with Playwright TypeScript for a detailed understaning on assertions. Summary Global setup in Playwright runs once before the entire test suite and is primarily used for preparing the test environment before running the tests. It helps keep tests independent and avoids relying on execution order. However, since it runs only once and does not directly share variables with tests, any required data must be passed through files, environment variables, or external storage mechanisms. Happy testing!
Meta is making another push into the frontier AI race, introducing two models designed to compete with OpenAI and Anthropic in coding, complex reasoning, and autonomous tasks. The company announced Monday that Muse Wonder and Muse Wonder Full are its most capable AI models to date. Built for personal and professional AI agents, the models are designed to handle tasks ranging from software development to document analysis and financial research. The launch follows about 14 months of work to rebuild Meta's AI capabilities under Meta Superintelligence Labs, led by Chief AI Officer Alexandr Wang. The company developed the models using its Prometheus data center and techniques spanning pretraining and reinforcement learning. The release also marks a change in Meta's approach. Unlike its earlier open-weight Llama models, neither Muse Wonder model is available for public download and self-hosting. Memory and persistence drive the new models Meta's new models feature two capabilities designed to make AI agents more useful for extended tasks. Active Memory lets the models retain relevant information across conversations and retrieve it when needed. Active Focus helps them work through obstacles, try alternative approaches, recover from errors and request clarification when necessary. The models are also designed to seek confirmation before important actions. These capabilities target a common weakness in AI agents: completing a multistep task requires more than producing a convincing answer. An agent must track requirements, use tools correctly and respond when its original plan fails. Meta says the models also deliver advanced coding performance and specialized knowledge in areas such as law, finance and health. Benchmark results put Meta back in contention Independent evaluations from Artificial Analysis give Muse Wonder Full a score of 56 on its Intelligence Index, tying it with Anthropic's Claude Sonnet 5.5. Muse Wonder scores 52, while OpenAI's GPT-6 Astra scores 53. The results suggest Meta has become more competitive with established frontier AI providers. However, the models do not lead every evaluation, and benchmark performance does not guarantee reliable results in real business environments. Benchmark rankings also tell only part of the story. For businesses evaluating AI agents, reliability, cost, data handling, and performance on real-world tasks may matter just as much as a model's overall score. Access comes with pricing and privacy limits Muse Wonder is available to developers through Meta's Model API and Muse Code programming assistant. The API costs $4 per million input tokens and $20 per million output tokens, with discounted rates for cached inputs. Muse Wonder Full is restricted to Muse Code subscribers, with subscription plans priced at $50 and $200 per month. Meta has not announced API access for the more capable model. Both models will eventually power features in Meta's Muse personal AI app, initially for subscribers. What the launch means for businesses and users For businesses, Muse Wonder adds another option for automating software development, document analysis, and multi-step workflows. Its pricing gives developers a concrete basis for testing it against competing models, although total costs will depend on how much reasoning, tool use and repeated processing each task requires. Privacy will be equally important. Meta says the models are trained to protect private information and resist harmful requests. However, VentureBeat reports that Muse Code subscriptions default to a Contributor mode that allows submitted material to be used to improve future models. Subscribers can disable that setting, while the standard API does not use submitted content for training. The different access options also come with different data-handling considerations. According to VentureBeat, Muse Code subscriptions default to a Contributor mode that allows submitted material to be used to improve future models, although subscribers can disable the setting. The standard Model API, by contrast, reportedly does not use submitted content for training. Consumers may benefit when Muse Wonder reaches the Muse app, particularly for tasks that require remembering previous conversations or recovering from setbacks. But persistent memory and access to personal messages, calendars and documents also raise questions about permissions and data handling. Meta says a fuller safety evaluation report is forthcoming. Until more evidence is available, users and organizations should verify consequential outputs and carefully review access permissions before allowing an AI agent to act on their behalf. Editor’s note: This article originally appeared on our sister publication, TechRepublic.
The project AngularAndSpring has been refactored from Spring Reactor to Spring blocking APIs using JDK 25 and virtual threads. The Old Architecture The reason AngularAndSpring was built using Spring Reactor was primarily the efficient use of operating system threads and memory under high load. Spring Reactor had a limited thread pool (usually the number of available CPU cores) and used OS threads only when the CPU had work to do. It did not let the operating system threads sleep. This used CPU resources much better and required far fewer OS threads, each of which needs around a megabyte of memory. The trade-off was a reactive programming style, which can become harder to read as the complexity of the implemented functionality grows, along with stack traces that are very difficult to read. These hard-to-read stack traces make finding and fixing errors much harder and significantly more time-consuming. How Virtual Threads Change the Trade-Off Before JDK 25, the choice was between efficient hardware use with Spring Reactor and easy-to-read code with easier debugging and readable stack traces. Spring's support for Java virtual threads offers the use of very efficient virtual threads for request processing. With virtual threads, operating system threads are used only when processing is needed; virtual threads are unmounted from the OS thread, freeing the OS thread to run a different virtual thread; and virtual threads have minimal memory requirements. That means a limited number of operating threads is able to handle a very large number of virtual threads. With JDK 25 and the virtual threads update, the choice has changed: JDK 25 with virtual threads uses hardware resources efficiently, and you can keep the code easy to read and debug with readable stack traces. There are limitations for virtual threads when native code is called, but the same limitations apply for Spring Reactor too. The main virtual threads blocker: The missing synchronized support was fixed in JDK 25. That means the main advantages of Spring Reactor are now provided by the JVM with its virtual threads. The New Architecture The basic building blocks of the new architecture can be found in the backend pom.xml. It now uses these starters: XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-restclient</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> The virtual threads are switched on with this in the application.properties: Properties files spring.threads.virtual.enabled=true The spring-boot-starter-webmvc is used to provide REST endpoints with blocking APIs. The spring-boot-starter-restclient is used to provide the new REST client for calling the blocking REST endpoints of other systems. The spring-boot-starter-data-mongodb is used to provide repositories and APIs for accessing MongoDB; all of these APIs are blocking. The ‘spring.threads.virtual.enabled=true’ enables the Spring Boot support for virtual threads provided by the JVM. The REST Endpoint The blocking REST endpoints now look like this in BitstampController: Java @RestController @RequestMapping("/bitstamp") public class BitstampController { private final BitstampService bitstampService; public BitstampController(BitstampService bitstampService) { this.bitstampService = bitstampService; } @GetMapping("/{currpair}/orderbook") public String getOrderbook(@PathVariable String currpair) { return this.bitstampService.getOrderbook(currpair); } ... } These endpoints do not return the reactor wrapper types. It just returns the intended return type. The ‘BitstampService’ methods now also return the intended return types without any wrappers. These endpoints just use plain Java. The Restclient The new blocking RestClient is now the Spring RestClient and looks like this in ScheduledTask: Java @Component public class ScheduledTask { private static final Logger LOG = LoggerFactory.getLogger(ScheduledTask.class); private static final String URLBS = "https://www.bitstamp.net/api"; private static final String URLCB = "https://api.coinbase.com/v2"; private static final String URLBF = "https://api.bitfinex.com"; private final BitstampService bitstampService; private final BitfinexService bitfinexService; private final CoinbaseService coinbaseService; private final MyUserService myUserService; private final RestClient.Builder restClientBuilder; public ScheduledTask(BitstampService bitstampService, MyUserService myUserService, BitfinexService bitfinexService, CoinbaseService coinbaseService, RestClient.Builder restClientBuilder) { this.bitstampService = bitstampService; this.bitfinexService = bitfinexService; this.coinbaseService = coinbaseService; this.myUserService = myUserService; this.restClientBuilder = restClientBuilder; } ... @Async("clientTaskExecutor") @Scheduled(fixedRate = 60000, initialDelay = 3000) @SchedulerLock(name = "BitstampQuoteBTC_scheduledTask", lockAtLeastFor = "PT50S", lockAtMostFor = "PT55S") public void insertBitstampQuoteBTC() { String currPair = "btceur"; insertBsQuote(currPair); } private ClientHttpRequestFactory customRequestFactory() { HttpClientSettings settings = HttpClientSettings.defaults() .withConnectTimeout(Duration.ofSeconds(4)) .withReadTimeout(Duration.ofSeconds(5)); return ClientHttpRequestFactoryBuilder.detect().build(settings); } private void insertBsQuote(String currPair) { LocalTime start = LocalTime.now(); try { QuoteBs quote = this.restClientBuilder.requestFactory(this.customRequestFactory()).build().get() .uri(String.format("%s/v2/ticker/%s/", ScheduledTask.URLBS, currPair)) .accept(MediaType.APPLICATION_JSON).retrieve().body(QuoteBs.class); quote.setPair(currPair); quote = this.limitPrecision(quote); this.bitstampService.insertQuote(quote); this.logDuration("Bitstamp", currPair, start); } catch (Exception e) { LOG.warn(String.format("Bitstamp data import failed for: %s", currPair), e); } } ... } The RestClient is created using the RestClientBuilder to allow connect and read timeouts to be configured. The timeouts are created in the customRequestFactory() method and set as a parameter of the RestClientBuilder. The RestClient uses the .uri(...) method to set the URL to call. The .accept(...) method sets the MediaType in the HTTP header that is expected from the REST client. The .retrieve() method executes the REST call, and the .body(...) method specifies the object into which the JSON response should be mapped. For debugging purposes, it can be useful to map the response body to a String first and analyze the response to ensure that it can be mapped correctly to the appropriate DTOs. The MongoDB Client For accessing the database, Spring Data MongoDB is used. The per-minute quotes are in the QuoteBs, QuoteBf, QuoteCb entities. The per-hour quotes are in the QuoteHourBs, QuoteHourBf, QuoteHourCb entities, and the per-day quotes are in the QuoteDayBs, QuoteDayBf, QuoteDayCb entities. The different entities enable Spring Data MongoDB support for each entity with its own repository. This is an example of QuoteBsMongoRepository: Java public interface QuoteBsMongoRepository extends MongoRepository<QuoteBs, ObjectId>, QuoteBsRepository { ... } The Interface ‘MongoRepository<QuoteBs, ObjectId>’ provides standard interfaces and enables the generated CRUD methods in QuoteBsRepository. Java public interface QuoteBsRepository extends QuoteRepository<QuoteBs>, QuoteMongoRepository<QuoteBs> { ... QuoteBs insert(QuoteBs quote); List<QuoteBs> findByPairAndCreatedAtAfterOrderByCreatedAtAsc(String pair, Date date, Limit limit); @Query("{ 'createdAt': { '$gt': ?0, '$lt': ?1 } }") List<QuoteBs> findByCreatedAtGreaterThanAndCreatedAtLessThan(Date from, Date to); } This interface uses the by-method-name-generated CRUD methods that Spring Data MongoDB provides and uses the ‘@Query’ annotation for a finder that can not be generated only by a method name. The entity QuoteBs looks like this: Java @Document public class QuoteBs implements Quote,QuotePairBs { @Id private ObjectId _id; @NotBlank @Indexed @JsonProperty private String pair; @NotNull @Indexed(name = "QuoteBs-createdAt") @JsonProperty private Date createdAt = new Date(); private BigDecimal high; private BigDecimal last; private Date timestamp; private BigDecimal bid; private BigDecimal vwap; private BigDecimal volume; private BigDecimal low; private BigDecimal ask; private BigDecimal open; The ‘@Document’ annotation marks the class as a MongoDB document. The ‘QuotePairBs’ interface is used to be able to access the common properties of minute, hour, and day entities of the same source. The ‘@Indexed’ annotation makes sure an index is created for this property in MongoDB for faster access and ordering. The precision of the Java BigDecimal type is higher than the precision of the MongoDB number type. Because of that, this method in ScheduledTask is used: Java private <T> T limitPrecision(T quote) { var fields = quote.getClass().getDeclaredFields(); for(Field field : fields) { if(field.getType().isAssignableFrom(BigDecimal.class)) { field.setAccessible(true); try { var value = (BigDecimal) field.get(quote); value = value.round(new MathContext(30, RoundingMode.HALF_UP)); field.set(quote, value); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } return quote; } The precision is limited to 30 decimals for all properties with the type BigDecimal. This is enough for all use cases in this application. Thread Pools If the hour and day quotes are recalculated from a large number of minute quotes, this can take several minutes. To avoid competing with the thread pool handling the user requests, a separate thread pool is created for this task in SchedulingConfig: Java @Bean(name = "clientTaskExecutor") public Executor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setVirtualThreads(true); executor.setThreadNamePrefix("clientTask-"); return executor; } This creates a separate TaskExecutor that can be used in the ‘@Async(“clientTaskExecutor”)’ annotations. That makes sure that no user requests wait in the queue for a free thread while long-running threads are at work. Conclusion With virtual threads in JVM 25 and Spring’s support for them, many reasons for using reactive programming in Spring have disappeared. The synchronized issue with virtual threads has been fixed in JDK 25, and the remaining causes of thread pinning are not relevant anymore for most use cases. The JVM now provides scalability and efficiency of reactive frameworks out of the box, while the blocking programming model is easier to implement and debug. Spring Data MongoDB support makes database access much easier with generated CRUD methods and the ‘@Query(…)’ support. It is time for projects still using reactive programming to move to an easier, faster programming model for developers.
DeepSeek is looking beyond chatbots and toward robots, with plans to lead a funding round for a young Chinese AI startup reportedly valued at $400 million. The Chinese AI lab has agreed to lead a seed investment in Winverse AI, a startup developing technology for robots, according to Bloomberg. The round is expected to raise tens of millions of dollars and would value Winverse at approximately $400 million after the investment. Meituan’s Long-Z Investments is among the other investors that have committed to the round. However, the deal has not been finalized, and both the valuation and investor participation could change, Bloomberg reported. Winverse was founded in 2026 by Hu Han, a former Tencent scientist who led the company’s vision-language and embodied AI foundation model teams. Hu previously worked at Microsoft Research Asia and Baidu, according to Bloomberg. From chatbots to machines that can act The potential investment would extend DeepSeek’s robotics ambitions beyond its existing relationship with Unitree Robotics. DeepSeek acquired a roughly 2% stake in the humanoid robot maker ahead of its Shanghai listing in August and plans to collaborate with the company on embodied-intelligence models. The move comes as DeepSeek approaches a financing round of at least 80 billion yuan ($12 billion), primarily intended to fund AI model training and data centers. The additional capital could also help the company support startups working on technologies it wants to develop jointly. China has established a strong position in humanoid robot manufacturing, but building capable machines requires more than advanced hardware. Robots must interpret instructions, understand their surroundings and execute tasks reliably in changing physical environments. That challenge has also attracted major investment in the United States. Skild AI and Physical Intelligence, which develop general-purpose AI for robots, have reached reported valuations of $14 billion and $5.6 billion, respectively, per Bloomberg. However, Winverse's precise technology remains unclear. The startup is operating in stealth, and no detailed product plans or technical architecture have been publicly disclosed. BigGo Finance reported that some industry insiders view its focus as closer to underlying infrastructure and model acceleration than embodied AI in its strictest sense. What this could mean for businesses and users For manufacturers, more capable robot AI could eventually make automation useful for tasks that remain difficult to program, particularly work that requires interpreting instructions or responding to changing conditions. If the technology matures, businesses could have more options for automating factory operations beyond repetitive, tightly controlled movements. However, Winverse has not publicly disclosed detailed product plans, making it too early to determine how its technology might be used commercially. For DeepSeek, the reported investment points to a broader opportunity beyond chatbots and conventional AI assistants: developing systems that can connect AI models with physical-world applications. Backing a specialized startup could give the company access to expertise and new development opportunities without building every robotics capability internally. Editor’s note: This article originally appeared on our sister publication, TechRepublic.
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.
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!!
Agile
Career Development
Methodologies
Team Management
The Jobs Matrix for AI: Four Boxes Instead of 40 Tools
October 12, 2026
by Stefan Wolpers
CORE
Why Your GPU Fleet Is Both Full and Idle: Building a Capacity Orchestration Layer
October 9, 2026 by Ankit Sinha
Kill the Worker, Keep the Research: Build a Recoverable LangGraph Agent on Temporal
October 5, 2026
by Akhil Madineni
CORE
AI/ML
Big Data
Databases
IoT
Catching Query Regressions Before Your Users Do
October 15, 2026 by Deepesh Dhake
Frameworks
Java
JavaScript
Languages
Tools
Node and Capacity Spillover: What Happens When Kubernetes Runs Out of Room
October 15, 2026 by Kishor Patil
Catching Query Regressions Before Your Users Do
October 15, 2026 by Deepesh Dhake
9 Practical Ways to Fix Laravel Performance Issues in Production
October 15, 2026 by Ankit Khoiwal
Deployment
DevOps and CI/CD
Maintenance
Monitoring and Observability
Node and Capacity Spillover: What Happens When Kubernetes Runs Out of Room
October 15, 2026 by Kishor Patil
Global Setup in Playwright TypeScript for API Testing
October 15, 2026
by Faisal Khatri
CORE
Azure Key Vault Secrets That Reconcile Themselves in Go, With mamori
October 14, 2026
by Xavier Portilla Edo
CORE
AI/ML
Java
JavaScript
Open Source