Programming languages allow us to communicate with computers, and they operate like sets of instructions. There are numerous types of languages, including procedural, functional, object-oriented, and more. Whether you’re looking to learn a new language or trying to find some tips or tricks, the resources in the Languages Zone will give you all the information you need and more.
Mitigating Cache Stampedes in Dynamic API Translation Using Java 21 Virtual Threads
AGENTS.md Makes Your Java Codebase AI-Agent Ready
Knowing what people say about your product usually means checking Google News, scrolling through YouTube, and digging into different social media threads. That's three tabs, three interfaces, and no way to compare what you find. This tutorial builds a single dashboard that pulls brand mentions from all three sources using Python and SerpApi. By the end, you'll have a Streamlit app with three tabs, one for news articles, one for YouTube videos, and one for social media and forum discussions. We'll use "serpapi" as the search query, but you can swap the brand or product name. Brand monitoring dashboard showing metrics row with total mentions, news articles, YouTube videos, and perspectives counts Set Up Your Environment Requirements: Python 3.8+SerpApi API Key (the free plan includes 250+ searches/month)Dependencies (serpapi, pandas, streamlit, altair) The serpapi package is the official Python SDK. It handles request signing, retries, and response parsing. The complete code, including a Jupyter notebook version, is available in the SerpApi tutorials repository. The Pipeline The app follows the same three-step pattern from the GitHub Issues dashboard: fetch raw data, transform it, and display the analysis. Pipeline diagram showing three stages: fetch, transform, and display The difference this time is three separate engines running in parallel. Each returns a different response structure, so the transform step normalizes everything into DataFrames before the dashboard consumes it. Fetch the Data A single SerpApi client instance works for all three engines: Python import serpapi import os SERPAPI_KEY = os.environ.get("SERPAPI_KEY", "") client = serpapi.Client(api_key=SERPAPI_KEY) Google News The Google News API returns articles through the news_results key. Each result includes title, link, source (a dict with name and icon), date, and snippet. Python def fetch_news(client, brand): """Fetch news articles mentioning the brand via Google News.""" results = client.search({ "engine": "google_news", "q": brand, "gl": "us", "hl": "en", }) return results.get("news_results", []) For more use cases with this engine, refer to the news monitoring. YouTube The YouTube Search API uses search_query instead of q, and the sp parameter controls time filters. The values EgIIAw%3D%3D (this week) and EgIIBA%3D%3D (this month) are YouTube's internal encoding for upload date filters. You can grab these from YouTube's URL bar after applying a filter manually. We run both filters and deduplicate by link, since the month results include everything from the week: Python YT_FILTER_WEEK = "EgIIAw%3D%3D" YT_FILTER_MONTH = "EgIIBA%3D%3D" def fetch_youtube(client, brand): """Fetch YouTube videos, combining week and month filters.""" seen = set() videos = [] for sp_filter in (YT_FILTER_WEEK, YT_FILTER_MONTH): results = client.search({ "engine": "youtube", "search_query": brand, "sp": sp_filter, }) for video in results.get("video_results", []): link = video.get("link", "") if link and link not in seen: seen.add(link) videos.append(video) return videos For more examples using the YouTube API, refer to this link. Google Perspectives Google Perspectives API surfaces user-generated content from LinkedIn, Reddit, Quora, and blogs. It uses the standard Google engine, and the results appear under the perspectives key: SerpApi search with the Google perspective results Python def fetch_perspectives(client, brand): """Fetch user-generated content (Reddit, LinkedIn, Quora).""" results = client.search({ "engine": "google", "q": brand, "google_domain": "google.com", }) return results.get("perspectives", []) Fetch in Parallel Three sequential API calls take roughly three seconds. Running them in parallel with Python ThreadPoolExecutor brings that down to about one second. Each call runs in its own thread while the others wait for their response: Python from concurrent.futures import ThreadPoolExecutor @st.cache_data(ttl=300) def fetch_all_mentions(brand): """Fetch all brand mentions from three engines in parallel.""" client = serpapi.Client(api_key=SERPAPI_KEY) with ThreadPoolExecutor(max_workers=3) as pool: news_future = pool.submit(fetch_news, client, brand) yt_future = pool.submit(fetch_youtube, client, brand) persp_future = pool.submit(fetch_perspectives, client, brand) return news_future.result(), yt_future.result(), persp_future.result() SerpApi also offers a server-side async parameter for large-scale batch processing, where you submit searches and retrieve results later. For our three concurrent calls, client-side threading is simpler and equally effective. The @st.cache_data(ttl=300) decorator caches results for 5 minutes. Without it, every Streamlit interaction would re-trigger the API calls. This works alongside SerpApi's own 1-hour result cache, which serves identical queries from the cache at no extra search cost unless you explicitly pass no_cache=true. Together, these two layers minimize redundant API calls during development and testing. For more optimization techniques when working with SerpApi at scale, refer to this blog. Transform the Data All three engines return dates as relative strings ("3 hours ago", "2 days ago"). We need a shared parser to convert them into datetime objects for sorting. Parse Relative Dates Two details worth noting. The regex is compiled once and reused since this function runs for every result in all three engines. And the fallback returns datetime.now(timezone.utc) instead of None, so results without a parseable date sort to the top rather than breaking pandas operations. Python import re from datetime import datetime, timedelta, timezone RELATIVE_DATE_RE = re.compile( r"(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago", re.IGNORECASE ) UNIT_TO_TIMEDELTA = { "second": lambda n: timedelta(seconds=n), "minute": lambda n: timedelta(minutes=n), "hour": lambda n: timedelta(hours=n), "day": lambda n: timedelta(days=n), "week": lambda n: timedelta(weeks=n), "month": lambda n: timedelta(days=n * 30), "year": lambda n: timedelta(days=n * 365), } def parse_relative_date(text): """Convert '3 hours ago' into a datetime object.""" if not text: return datetime.now(timezone.utc) match = RELATIVE_DATE_RE.search(str(text)) if not match: return datetime.now(timezone.utc) amount = int(match.group(1)) unit = match.group(2).lower() delta = UNIT_TO_TIMEDELTA.get(unit, lambda n: timedelta())(amount) return datetime.now(timezone.utc) - delta Build DataFrames Each engine gets into its own transformer. Here's the news version: Python def transform_news(results): """Convert raw Google News results into structured records.""" records = [] for item in results: source = item.get("source") or {} source_name = source.get("name", "Unknown") if isinstance(source, dict) else str(source) records.append({ "title": item.get("title", ""), "link": item.get("link", ""), "source": source_name, "date": parse_relative_date(item.get("date", "")), "snippet": item.get("snippet", ""), }) return records The source field can be a dict or a plain string depending on the result, so the isinstace check handles both. YouTube and Perspectives follow the same pattern, with two differences worth highlighting. YouTube views come back as strings like "1,234 views", so we strip non-numeric characters before converting: Python views = item.get("views") or 0 if isinstance(views, str): views = int(re.sub(r"[^\d]", "", views) or 0) Build the Dashboard The Streamlit interface starts with a form for the brand query and a row of summary metrics across all three sources: Python st.set_page_config(page_title="Brand Monitoring Dashboard", layout="wide") st.title("Brand Monitoring Dashboard") with st.form("brand_form"): brand = st.text_input("Brand or keyword to monitor", value="serpapi") submitted = st.form_submit_button("Search") Brand or keyword selector to monitor After fetching, the dashboard shows four metrics at the top for a quick overview, then splits into three tabs: Python col1, col2, col3, col4 = st.columns(4) col1.metric("Total Mentions", total_mentions) col2.metric("News Articles", len(news_records)) col3.metric("YouTube Videos", len(yt_records)) col4.metric("Perspectives", len(persp_records)) Dashboard metrics row displaying total mentions across three sources News Tab The News tab pairs an Altair bar chart of top sources with a sortable table. Altair ships with Streamlit, so there's nothing extra to install. We use it instead of st.bar_chart because it gives control over orientation, tooltips, and styling. Python source_df = news_df["source"].value_counts().head(10).reset_index() source_df.columns = ["source", "count"] source_chart = alt.Chart(source_df).mark_bar( cornerRadiusTopRight=4, cornerRadiusBottomRight=4 ).encode( x=alt.X("count:Q", title="Articles"), y=alt.Y("source:N", sort="-x", title=""), color=alt.value("#4A90D9"), tooltip=["source:N", "count:Q"], ).properties(height=350) st.altair_chart(source_chart, use_container_width=True) News tab with horizontal bar chart of top sources and sortable article table The table uses st.column_config.LinkColumn so each article title links directly to its source. YouTube Tab The YouTube tab shows views by channel and a sorted video table. The chart groups views by channel to surface which creators talk about the brand the most. Python channel_df = yt_df.groupby("channel")["views"].sum().reset_index() channel_df = channel_df.sort_values("views", ascending=False).head(10) channel_chart = alt.Chart(channel_df).mark_bar( cornerRadiusTopRight=4, cornerRadiusBottomRight=4 ).encode( x=alt.X("views:Q", title="Views", axis=alt.Axis(format="~s")), y=alt.Y("channel:N", sort="-x", title=""), color=alt.value("#4A90D9"), tooltip=["channel:N", alt.Tooltip("views:Q", format=",")], ).properties(height=350) YouTube tab showing views by channel chart and video table Perspectives Tab The Perspectives tab splits the layout between a discussion table on the left, and a donut chart of mentions by platform on the right. The donut chart makes it easy to see where conversations happen, whether it's LinkedIn, Reddit, X, etc. Python platform_chart = alt.Chart(platform_df).mark_arc( innerRadius=60, outerRadius=120 ).encode( theta=alt.Theta("count:Q"), color=alt.Color("source:N", legend=alt.Legend(title="Platform")), tooltip=["source:N", "count:Q"], ).properties(height=350) Perspectives tab with discussions table on the left and donut chart of mentions by platform on the right When to Use This Approach Ideal for: Tracking brand mentions across news, video, and social in one viewMonitoring product launches, PR campaigns, or competitor namesBuilding internal dashboard for marketing or DevRel teams Not recommended for: Real-time alerting. The API returns a snapshot, not a stream. For notifications, schedule the script on an interval and compare results.Historical analysis. Each engine returns recent results, not a complete archive. If you want to explore the API response before writing code, the SerpApi Playground lets you test any engine interactively. And if you only need news coverage, the Google News API alone handles most brand monitoring use cases. Where to Go from Here This dashboard gives you a live snapshot. The natural next step is turning it into a historical record. Store each fetch in a database (SQLite, PostgreSQL, or even a CSV), and you can compare mention volume week over week, track which sources cover your brand consistently, and spot trends that a single snapshot can't show. With historical data in place, you can layer on more analysis. Identify content gaps by looking at what topics competitors get covered on, but you don't. Track which YouTube channels mention your product and how their view counts trend over time. Flag new platforms or authors that start discussing your brand. The data is yours to work with however fits your needs. The three engines give you the raw material; what you build on top depends on the questions you're trying to answer. Conclusion The full application is about 350 lines in a single Python file. Three API calls, three DataFrames, three tabs. The query input at the top lets you switch brands without changing the code. What started as a way to check where "serpapi" shows up on the web became a tool that surfaces patterns you miss manually. The Perspectives tab pulls in LinkedIn posts, Reddit threads, and Quora answers that don't appear in regular news or video searches, and combining them in one view gives you the full picture. Check out the full SerpAPI article collection here.
Modern applications rarely rely on a single data model. Relational databases remain essential for transactional consistency and structured business data. However, document, key-value, column-oriented, graph, and vector databases are now critical for workloads that require flexible schemas, horizontal scalability, low-latency access, or specialized queries. As a result, polyglot persistence — selecting the most appropriate database model for each use case — has become a standard architectural strategy rather than an exception. The rise of artificial intelligence further supports this trend. Retrieval-augmented generation (RAG), semantic search, recommendation systems, and autonomous agents often rely on embeddings and vector similarity searches to access contextual information. As a result, vector databases and multimodel NoSQL platforms are becoming integral to the modern enterprise data landscape. In this context, Jakarta NoSQL offers Jakarta EE developers a standardized and extensible programming model for working with various NoSQL technologies, while minimizing direct dependence on specific database vendors. From Jakarta NoSQL to Polyglot Persistence Jakarta NoSQL is the first specification developed within the Jakarta EE ecosystem, rather than inherited from Java EE. It addresses the need for enterprise applications to use NoSQL databases and supports polyglot persistence. Its goal is to offer a simple, vendor-neutral programming model for document, key-value, column, and graph databases, so developers do not need to learn a separate API for each provider. This work influenced the development of Jakarta Data, which introduced a repository-oriented model independent of database technology, and Jakarta Query, which aims to provide a unified query language across persistence specifications. Collectively, these specifications advance Jakarta EE toward a broader and more consistent data-access strategy. Entity mapping is the initial step in Jakarta NoSQL. Its annotations use terminology familiar from Jakarta Persistence, formerly JPA. Developers use @Entity to define persistent types, @Id for keys, and @Column for attributes. This consistency lowers the learning curve for Java developers experienced with Jakarta Persistence. For example, an investment can be modeled as follows: Java ackage expert.os.videos.nosql; import jakarta.nosql.Column; import jakarta.nosql.Entity; import jakarta.nosql.Id; import java.math.BigDecimal; import java.util.UUID; @Entity public class Investment { @Id private UUID id; @Column private String name; @Column private InvestmentType type; @Column private BigDecimal amount; public Investment( UUID id, String name, InvestmentType type, BigDecimal amount) { this.id = id; this.name = name; this.type = type; this.amount = amount; } Investment() { } @Override public String toString() { return "Investment{" + "id=" + id + ", name='" + name + '\'' + ", type=" + type + ", amount=" + amount + '}'; } } ublic enum InvestmentType { STOCK, BOND, FUND, CRYPTO, REAL_ESTATE } Jakarta NoSQL supports Java records, enabling entities to be defined in a more concise and immutable format: Java @Entity public record Investment( @Id UUID id, @Column String name, @Column InvestmentType type, @Column BigDecimal amount) { } A key difference from Jakarta Persistence is that persistent attributes must be explicitly marked with @Id or @Column. Fields lacking these annotations are ignored, making the persistence model clearer and preventing accidental storage of attributes. After mapping the entity, it can be inserted, retrieved, and queried using the template API: Java UUID id = UUID.randomUUID(); Investment investment = new Investment( id, "Java Growth Fund", InvestmentType.FUND, new BigDecimal("1500.00") ); template.insert(investment); template.find(Investment.class, id) .ifPresent(System.out::println); template.select(Investment.class) .where("amount") .gt(new BigDecimal("1000")) .result() .forEach(System.out::println); The fluent query API makes operations easy to discover and keeps queries aligned with the domain model. In this example, the application uses Oracle NoSQL, but the same mapping and structure can be reused with providers like MongoDB or ArangoDB by updating dependencies and connection settings. The common API reduces vendor coupling, though database-specific features such as transactions, consistency, indexing, and advanced queries may still require provider-specific solutions. Jakarta NoSQL 1.1 Jakarta NoSQL 1.1 advances data access in Jakarta EE by improving compatibility with other specifications. With Jakarta EE 12, enterprise Java enters a new data era, highlighted by Jakarta NoSQL’s integration with Jakarta Query. Jakarta Query provides a unified query model for Java applications and diverse data sources. Its core language defines essential query concepts such as entities, attributes, comparisons, filtering, and parameters. It also offers the Jakarta Persistence Query Language, previously known as JPQL, enabling its familiar syntax and concepts to be used by other specifications and persistence technologies. With the Investment entity, applications can execute string-based queries directly using the template API: Java template.query("FROM Investment WHERE amount > 1000") .result() .forEach(System.out::println); Queries can use named parameters to separate values from the query expression: Java template.query("FROM Investment WHERE amount > :amount") .bind("amount", new BigDecimal("1000")) .result() .forEach(System.out::println); Jakarta NoSQL 1.1 supports projections, enabling queries to return only the information needed for a specific use case rather than loading the entire entity. Projections can be represented as Java records and declared with the @Projection annotation: Java @Projection public record InvestmentProjector( String name, BigDecimal amount) { } The projection can then serve as the result type for a typed query: Java template.typedQuery( "FROM Investment WHERE amount > 1000", InvestmentProjector.class) .result() .forEach(System.out::println); In this example, the query returns only the investment name and amount. This approach is useful for reports, dashboards, API responses, and other read-oriented scenarios where retrieving the full entity is unnecessary. Records are well-suited for projections because they offer a compact and immutable representation of selected data. Jakarta NoSQL 1.1 expands the fluent API. Previous versions supported select and delete operations: Java template.select(Investment.class) .where("amount") .gt(new BigDecimal("1000")) .result() .forEach(System.out::println); template.delete(Investment.class) .where("amount") .gt(new BigDecimal("1000")) .execute(); Version 1.1 adds fluent update operations, completing the main set of data manipulation capabilities: Java template.update(Investment.class) .set("amount") .to(new BigDecimal("2000.00")) .where("id") .eq(id) .execute(); This operation updates matching entities directly, eliminating the need to retrieve and modify them in memory first. Another enhancement is the autoApply attribute for the @Converter annotation. When enabled, the converter is automatically applied to every mapped attribute of the supported Java type, removing the need to declare it on each field. This reduces repetitive configuration and ensures consistent custom type conversion across the domain model. Together, Jakarta Query integration, projections, fluent update operations, and automatic converters make Jakarta NoSQL 1.1 more expressive and better aligned with the broader Jakarta EE data ecosystem.
Compliance-reporting teams keep spreadsheets in the loop for a practical reason: a workbook lets domain experts inspect assumptions, formulas, source rows, and intermediate values without reading a line of application code. That transparency is genuinely useful, and it's a big part of why replacing Excel outright so often fails to stick. The trouble starts once that workbook becomes part of a repeatable, audited reporting process — a regulatory filing, an IFRS report, a periodic compliance submission. At that point, a shared Excel file isn't enough on its own. What's actually needed is version control, validation, an audit trail, a review step, and a reliable way to connect the spreadsheet's logic to the systems downstream. The spreadsheet itself isn't the problem. It's a review surface domain experts genuinely need. The problem is treating it as a loose file sitting outside the application. The goal isn't to eliminate spreadsheets, but to preserve the spreadsheet experience while letting the application govern how it's used. This article walks through an architecture that keeps the workbook where domain experts can see it, but moves execution — validation, calculation, output generation, logging — into a Java application. The scenario is inspired by a real-world IFRS reporting project, and the same architecture applies to regulatory reporting, statutory filings, actuarial review, and other spreadsheet-driven compliance workflows. The pattern itself doesn't require a specific product: it works with any spreadsheet engine that can load a workbook and expose read/write access to Java, and parts of it apply even if you only use a file library like Apache POI at the edges. Three Ways Teams Usually Respond Rewrite everything in Java. Engineering gets control, tests, and CI. But the calculation logic moves away from the people who understand it. Every threshold change, every new currency, every adjusted formula now goes through a sprint. Sometimes that's correct — if the rules are stable and nobody inspects formulas, do this. For living, business-owned logic, it breeds shadow spreadsheets. Leave the desktop spreadsheet alone. Finance keeps full flexibility. The organization keeps none of the guarantees: no version control, no audit trail, no way to prove which file produced the submitted numbers. Use a file library only at the edges. Java imports the workbook, exports the results. Better — but the correction loop still happens in desktop Excel: download, fix locally, re-upload, re-validate, repeat. Every round trip is an audit gap. Now there is a fourth option: embed the workbook directly into the web application. Domain experts continue working in a familiar spreadsheet interface, while the application governs when users can edit data, when validation runs, which outputs become visible, and how every operation is logged. The rest of this article is about what that looks like in practice. The Big Idea: One Workbook, Two Roles In this pattern, the workbook plays two roles at the same time: For users, it is the interface. They inspect rows, correct values, maintain rule tables, and review generated outputs in a familiar grid.For the application, it is a runtime artifact. Java loads a known template, reads specific sheets and regions, runs validation, writes outputs, and records every run. The design decision that makes this work: Java never wanders through the workbook looking for data. It reads and writes only through agreed sheets and regions — a contract. Finance owns what's inside the regions: values, formulas, rules. Engineering owns the boundary and everything behind it: execution, permissions, persistence, export. Let's see the two stages of a typical reporting workflow through this lens. Stage 1: Let Users Fix Data Issues Without Leaving the App Reporting source data almost never arrives clean. A currency code says US instead of USD. An FX rate is missing. A service fee breaks a policy limit. The template for this stage has two sheets. Input CSV holds the source rows users can inspect and correct. ETL Rule holds the validation rules — as an ordinary spreadsheet table with columns like Field, Check, and Allowed Values. A rule row might say: currency must be one of USD, EUR. Finance can read and change these rules without asking anyone. When the user clicks Run Validation, the application takes over. To make this concrete: the examples in this article use Keikai Spreadsheet, a Java-based spreadsheet UI component, to embed the workbook in the browser and read and write it from Java — though the same three-step logic applies with any comparable engine. Conceptually, the Java service reads the data rows, reads the rule rows, and checks every row against every rule: Java List<SourceRow> rows = sheetReader.readTable(workbook, "Input CSV"); List<Rule> rules = ruleParser.parse(sheetReader.readTable(workbook, "ETL Rule")); for (SourceRow row : rows) for (Rule rule : rules) rule.check(row).ifPresent(report::add); Notice what this is not: the rules are not hard-coded in Java. Java only knows how to read the rule table and apply generic checks. The actual business knowledge — which currencies are allowed, what a valid fee looks like — stays in the workbook where its owners can see it. One detail carries most of the user experience: every validation error records which cell failed — sheet, row, and column. That lets the UI show a panel saying “policy P-1024, field currency, value US, expected USD or EUR” with a link that jumps the user straight to the offending cell. They fix it in the grid, click run again, and validation passes. Compare that to the traditional loop — download, fix in Excel, upload, pray. Here, nothing leaves the system, and every edit can be logged with user, timestamp, old value, and new value. Stage 2: Generate Outputs Under Application Control Once the data is clean, the second stage produces the actual reporting outputs: journal entries, impact tables, export-ready CSV sheets. The input is a policy sheet with assumptions (premium totals, fees, FX rates) plus a rule table that maps accounting events to journal lines. Before the run, the application shows only the input sheet — output sheets stay hidden, because they don't exist meaningfully yet. When the user triggers generation, the same Keikai-backed workbook is read and written from Java: it reads the inputs, computes the metrics, builds the journal rows, and writes them back into the workbook: Java PolicyInput policy = policyReader.read(workbook, "Policy Input"); Metrics metrics = deriveMetrics(policy); // plain Java arithmetic List<JournalRow> rows = journalBuilder.build(readJournalRules(workbook), metrics); sheetWriter.replaceTable(workbook, "Journal Entries", rows); revealSheets(workbook, "Journal Entries", "Report Impact", "Journal CSV"); The interesting part is the last line. Sheet visibility is an application decision: outputs appear only after a successful run, so a reviewer can never mistake stale output for fresh output. The reviewer then sees everything in one place — assumptions, rules, generated journals, report impact — in the same grid, and the export button produces a file the application has logged and versioned. deriveMetrics itself is deliberately simple — a handful of multiplications and subtractions. In a real system it may be far more complex, or it may even delegate back to formulas in the workbook. The architecture doesn't change: inputs go into agreed regions, outputs come from agreed regions, and Java owns the trigger. The Part Everyone Skips: The Workbook Is Now an API The moment Java code depends on a sheet named ETL Rule with a header called Allowed Values, the workbook has stopped being a document. It has become an interface — and interfaces break when they're changed casually, without review. The fix is to make the contract explicit and test it. Distinguish two kinds of change: Value changes – a new allowed currency, an adjusted threshold, a reviewed formula edit. These live inside the contract. Finance can make them without touching Java.Structural changes – renaming a sheet, deleting a header, moving an output table three columns right. These are API changes and should be reviewed like one. Then write this test: Java @Test void templateSatisfiesReportingContract() { Workbook wb = engine.load("reporting-template.xlsx"); assertSheetExists(wb, "Input CSV", "ETL Rule", "Journal Entries"); assertHeaders(wb, "ETL Rule", "Field", "Check", "Allowed Values"); } It looks almost too simple to matter, but most real-world workbook integration failures are exactly this mundane — a renamed sheet or a deleted header, discovered the night before a regulatory filing is due. Catching it in CI, before any template goes live, is what makes the difference. Finally, log runs, not just files: template version, who ran it, validation status, output row counts, a hash of the inputs. When someone asks “why does this quarter's filing look wrong?”, you answer from the run log instead of from archaeology on a shared drive. For compliance teams, these controls turn the workbook from an informal file into evidence the organization can explain. A reviewer can trace which template version produced a number, which source data was used, who ran the process, whether validation passed, and which output was exported. If a template structure changes, the contract test shows whether the workbook still satisfies the application’s required sheets and headers before it reaches production. In other words, the system does not just calculate results; it records the evidence needed to defend how those results were produced. When to Consider a Simpler Approach This approach pays off when the workbook is a genuine shared language between domain experts and developers — something both sides actually read, edit, and rely on. If the rules rarely or never need to change, and nobody inspects formulas, plain Java is simpler to test and operate. And if the workbook is really just a transfer format between systems, a straightforward import/export covers it. Takeaways The compliance-reporting spreadsheet doesn't have to be rewritten or worked around. Put it inside the application and split ownership along a clear line: The workbook owns what users must see and maintain: source rows, rule tables, assumptions, reviewable outputs.The application owns execution: validation, generation, sheet visibility, permissions, logging, export.The contract between them — named sheets, headers, regions — is documented, tested in CI, and changed only with review. Do that, and the workbook stops being an unversioned file nobody can fully account for. It becomes a governed part of the application — the place where domain experts and the system finally agree on the numbers.
In the first article, we got started with Jeffrey Microscope and learned to read a single flamegraph — the timeseries, search, tooltips, and the allocation and wall-clock variants. This time we build directly on that foundation and tackle one of Jeffrey's most powerful features for real-world performance work: the differential flamegraph, which compares two recordings and shows you precisely what changed between them. A single flamegraph tells you where your application spends its time. But the questions that matter most in practice are comparative: Did my optimization actually help?What did this refactor make slower?Where did the extra allocations come from? Staring at two flamegraphs side by side and trying to spot the difference by eye is slow and error-prone — the graphs are large, and the interesting change is often a few frames buried deep in the stack. Jeffrey Microscope's differential flamegraph solves this by overlaying two recordings into a single graph and coloring every frame by how it changed: Red – where the primary profile spends more than the baseline (a regression).Green – where it spends less (an improvement).Deeper shades – brand-new and fully-removed frames, called out distinctly. In this article, we'll take the two recordings from the previous post — the optimized direct serialization path and the garbage-heavy DOM path — set one as a secondary profile, and let the differential view pinpoint exactly which methods account for the difference. We start exactly where the first article left off. Open the optimized recording, jeffrey-persons-direct-serde-cpu.jfr.lz4, and head to the Visualization tab — this is our primary profile, the same CPU flamegraph we explored last time. On its own, it shows where the direct serialization path spends its time, but to turn it into a comparison we need a second recording to diff it against. That's what the Secondary Profile slot in the top bar is for — currently marked NOT SET. In the next step we'll point it at the DOM-based recording and unlock the Differential view in the sidebar. Supported Events Types With the secondary set, the Differential page mirrors the Primary one — a card per event type — but each now shows both sides at once. The value on the left is the baseline (the secondary profile), the value on the right is the primary, and the badge is the relative change from one to the other: a red +N% means the primary has more of that event than the baseline (grew), a green −N% means it has less (shrank). This lets you gauge the overall shift before opening a single graph — whether the change is a rounding-error wobble or a real regression worth investigating. Jeffrey supports differential flamegraphs for every sample-based event it can render normally: Execution Samples – total CPU work. More samples means more time spent on-CPU (37.3K → 39.7K, +6.4% here).Wall-Clock Samples – elapsed time including waiting and blocking, which can move independently of CPU (5.0M → 4.4M, −12.4%).Allocation Samples – memory pressure; switch Use Total Allocation to compare bytes rather than sample count and see the true allocation cost (27.47 GiB → 30.45 GiB, +10.9%).CPU-Time Samples and Method Traces – empty here, but diff identically when the recordings contain them. Each of these numbers is just the headline; the flamegraph below breaks the same delta down frame by frame, so you can see which methods drove it. Click View Flamegraph on the Execution Samples card to open the differential CPU view. Reading the Differential Flamegraph Opening the differential view feels familiar — same timeseries, search, and tooltip as a normal flamegraph — but everything now encodes two profiles at once: The summary bar at the top reports the totals side by side: baseline 35,472 vs primary 39,668, a net +4,196 (+11.83%) flagged as REGRESSED. That's the headline — the primary run did more on-CPU work overall.The timeseries overlays both recordings as two lines — Primary in blue, Secondary (baseline) in red — so you can see where in time the profiles diverge, not just that they differ.The flamegraph colors encode the per-frame change: pale pink/green for frames that shifted a little, and saturated deep red/deep green for frames that exist in only one profile — brand-new work versus work that disappeared entirely. The payoff is in the last two screenshots. Because the optimized and unoptimized paths run through differently-named classes, the diff renders them as a matched pair: the deep-red EfficientPersonService.getNPersons subtree (new in the primary) sitting right next to the deep-green InefficientPersonService subtree (gone from the primary). You're literally seeing the code swap, top to bottom. And hovering a shared frame quantifies it precisely — the tooltip on PersonController.getNPersons shows baseline 854 → primary 525, an IMPROVED −329 (−38.52%) for that endpoint's own path. The differential CPU flamegraph overlays both recordings: the timeseries plots the primary (blue) against the secondary baseline (red), and the summary bar reports baseline 35,472 → primary 39,668, a net +4,196 (+11.83%) marked REGRESSED. The merged flamegraph colors every frame by its change. The shared Tomcat, Coyote, and Spring layers stay mostly pale pink — small shifts — while the summary bar keeps the overall +11.83% delta in view. The flamegraph also captures the JVM's own threads, not just your request path — the CompileBroker / C2Compiler stacks on the left are JIT compilation, and garbage-collection activity shows up the same way. Comparing them across the two recordings tells you whether either run triggered extra spikes in JIT or GC work, a common hidden cost when one version allocates more or churns more code. Deeper into the stack, the two implementations separate out: saturated red columns mark work that is new in the primary profile, while the deep-green columns are paths that existed only in the baseline and disappear in the primary. The optimized EfficientPersonService path (red, added) sits beside the removed InefficientPersonService path (green). Hovering the shared PersonController.getNPersons frame quantifies the change exactly: baseline 854 → primary 525, an IMPROVED −329 (−38.52%). Summary From here, try the same workflow on the Wall-Clock and Allocation differential flamegraphs — the steps are identical, and each reveals a different dimension of the change: time spent waiting, and bytes allocated. Thank you for reading! To go deeper, visit the Jeffrey pages, or reach out to me directly on LinkedIn — I'd love to hear your feedback. And stay tuned: in the next article, we'll step away from flamegraphs and explore one of Jeffrey's JVM Internals views to dig into what the runtime does under the hood.
Picture this: you are a software developer building an education platform, and you receive from the product owner some requirements written in business language (Gherkin). You need to implement these scenarios in Python. Probably you will start creating models and service modules. You will create some classes to represent the entities described in the scenarios, like Student, Course, and Subject. You will add conditionals and loops in the entity classes to control the business logic and restrict paths in the code: Python # Enroll a student in a course if course.status == "active" and student.course == None: student.course = course raise BusinessError("Student already in a course") Also, you will create a class to represent the persistence layer (database) and methods like list_students, get_course_by_name, and create_student to add, delete, update, and return data from the database. You will probably create facades to group the classes in a logical sequence, add more ifs, elses, and loops to control the code flow. At the end of the sprint, you have a scenario implemented and tested. There is nothing wrong with its style of implementation. It is a common process. However, something loses importance in this process: the business scenario itself. In this article, I’ll showcase a behavior-driven development approach that converts business languages directly to executable code. The intention is to keep the implementation closer to the business language and promote the code to the source of truth. Gherkin Scenarios for the Education Platform Going back to the fictional (not much) story. Here are some scenarios an education platform could have: Gherkin Feature: Student GPA and approval Scenario: Student is approved when GPA is 7 or higher and all subjects are passed Given a student named "John" is enrolled in the "Computer Science" course And the course has the subjects "Math", "Physics", and "Programming" And the student has the following grades: | Subject | Grade | | Math | 7 | | Physics | 8 | | Programming | 9 | When the system calculates the student's GPA Then the GPA should be 8 And the student status should be "Approved" Feature: Student enrollment in course subjects Scenario: Student cannot enroll in a subject from another course Given a student named "Carlos" is enrolled in the "Medicine" course And the subject "Algorithms" belongs to the "Computer Science" course When the student tries to enroll in the subject "Algorithms" Then the enrollment should be rejected And the system should show the message "Students can only enroll in subjects from their own course" Feature: Student enrollment in a course Scenario: Student enrolls in an active course Given a course named "Architecture" is active When a student named "Julia" tries to enroll in the "Architecture" course Then the enrollment should be accepted And the system should show the message "Student enrolled in course" Feature: Course cancellation Scenario: Students cannot enroll in a canceled course Given a course named "Architecture" has been canceled by the general coordinator When a student named "Julia" tries to enroll in the "Architecture" course Then the enrollment should be rejected And the system should show the message "Canceled courses cannot accept new enrollments" They are pretty, readable, easy to understand, and find inconsistencies. Now, a possible implementation as described in the previous story. It was simplified for the sake of this article. Let us look at a more traditional implementation. Python # Entities class Course: def __init__(self, course_id, name): self.course_id = course_id self.name = name self.is_canceled = False class Student: def __init__(self, student_id, name): self.student_id = student_id self.name = name self.course = None # Application class UniversityService: def __init__(self): self.courses = {} self.students = {} def create_course(self, course_id, name): self.courses[course_id] = Course(course_id, name) def create_student(self, student_id, name): self.students[student_id] = Student(student_id, name) def cancel_course(self, course_id): course = self.courses.get(course_id) if course is None: raise ValueError("Course not found") course.is_canceled = True def enroll_student_in_course(self, student_id, course_id): student = self.students.get(student_id) course = self.courses.get(course_id) if student is None: raise ValueError("Student not found") if course is None: raise ValueError("Course not found") if course.is_canceled: raise ValueError("Canceled courses cannot accept new enrollments") student.course = course # Scenario: Students cannot enroll in a canceled course service = UniversityService() service.create_course("C1", "Architecture") service.create_student("S1", "Julia") service.cancel_course("C1") try: service.enroll_student_in_course("S1", "C1") print("Unexpected result: student enrolled in a canceled course") except ValueError as e: print(e) It was done in a traditional style. Notice the technical references like service and the preconditions and business logic spread in many ifs in the code. We forgot to represent the system behavior in a simple and explicit way. The scenario was spread into many pieces, and it may be hard to put all of them together when we need to understand the code in the future. Consider that more features will be integrated into the code, and more if/else statements will be introduced to control the business logic and new flows. In summary, the scenario cannot be read as it was presented by the business team. It is hard to validate that the system is doing what it should do without proper unit tests and careful code review. We can try to test its integration with Python Behave to bring the explicit behavior back to the game, but it may be hard to do it without coming up against technical stuff like services. The system works, but it is hard to prove that it behaves as expected just by reading the code. At this point, the development team and the business team are not talking the same language anymore. There is a translation from business language to production code (technical stuff). Behavior-Driven Development Now, using the framework Guará to represent the scenarios directly in the code. The code now tells the story. For example, the scenario Student enrollment in a course can be written like this: Python from guara.application import Application eduapp = Application() ( eduapp.given(IsActiveCourse, course_id=course_id) .and_(IsNotStudentInACouse, student_id=student_id) .when( EnrollStudentInCourse, student_id=student_id, course_id=course_id, ) .then(it.IsEqualTo, "Student enrolled in course") ) The preconditions IsActiveCourse and IsNotStudentInACourse are now explicit and are at a higher level of the code. Not buried in the methods in the form of if conditionals. The precondition and action classes have single responsibilities. Python from guara.transaction import AbstractTransaction class IsActiveCourse(AbstractTransaction): def do(self, course_id): print(f"Checking the status of course {course_id}") status = database.courses.get_status(course_id=course_id) if status == "Active": return True raise CourseCanceledException("Course canceled") class IsNotStudentInACourse(AbstractTransaction): def do(self, student_id): print(f"Checking if student in a course") course = database.student.get_course() if course: raise StudentException("Student already in a course") class EnrollStudentInCourse(AbstractTransaction): def do(self, student_id, course_id): print(f"Enrolling student {student_id} in course {course_id}") status = database.enroll_course(course_id, student_id) return "Student enrolled in course" In the end, it is easier to compare the code against the scenario steps and assert they are present in the code. Python import argparse from guara.transaction import Application from guara import it eduapp = Application() def main(): parser = argparse.ArgumentParser() parser.add_argument("--action", required=True) parser.add_argument("--student-id") parser.add_argument("--course-id") args = parser.parse_args() if args.action == "enroll_course": try: ( eduapp.given(HasCourse, course_id=args.course_id) .and_(IsActiveCourse, course_id=args.course_id) .and_(HasStudent, student_id=args.student_id) .and_(IsNotStudentEnrolledInCourse, student_id=args.student_id) .when( EnrollStudentInCourse, student_id=args.student_id, course_id=args.course_id, ) .asserts(it.IsTrue) ) except Exception as e: print(str(e)) app.undo() # Calling the CLI python edu.py enroll-course --course-id 10 --student-id 1324 Benefits The production code is now the source of truthIt can be compared directly to the business scenariosThe responsibilities are encapsulated in dedicated classesIt is possible to undo operations easily once the framework is based on the Command Pattern (GoF)It is easy to add more behavior to the code without changing other classesThe classes are reusableIt hides the technical stuff. They still exist, but now the actions are first-class citizens Points of attention It is not a one-size-fits-all style. It is necessary to evaluate whether the system under development will benefit from this code styleMakes more sense when the scenarios are defined in Gherkin language; otherwise, it will be necessary to translate the requirement to code as done in the traditional implementation Conclusion The important difference is that the source code still reads almost like the original Gherkin scenario. Instead of hiding business rules inside technical layers, we keep them visible and explicit in the code.
Java Flight Recorder (JFR) captures an enormous amount of detail about what your application is doing — but raw JFR files are only as useful as the tools you have to explore them. Jeffrey is an open-source JFR analyzer that specializes in turning JFR events into interactive visualizations, and Jeffrey Microscope is its standalone, single-user deployment: a self-contained application that lets you import recordings and dig into flamegraphs, timeseries, and other views right in your browser. Getting started takes a minute: Standalone JAR – download the latest microscope.jar from the GitHub releases page and start it with java -jar microscope.jar (Java 25 or newer).Docker – skip the setup entirely with docker run -it --network host petrbouda/microscope.Sample recordings – if you want to explore the tool before profiling your own application, the petrbouda/microscope-examples image ships with sample recordings preloaded (docker run -it --network host petrbouda/microscope-examples). In this article, we'll use Jeffrey Microscope to analyze JFR flamegraphs and walk through how they help you find where your application actually spends its time. Let's set up a hands-on environment. Download the latest microscope.jar from the GitHub releases page and launch it (Java 25 or newer): Shell java -jar microscope.jar Open it in your browser, then grab some recordings to analyze — Jeffrey maintains a companion repository of real JFR recordings captured from various serialization and profiling scenarios: Shell git clone https://github.com/petrbouda/jeffrey-recordings The files ship as compressed .jfr.lz4, which Jeffrey Microscope reads natively. Drag one onto the Drop Recordings zone on the dashboard — the upload starts automatically, and within a few seconds you have a profile ready to explore. For this walkthrough, we'll focus on two recordings that profile the same piece of code — an HTTP endpoint that serializes and deserializes JSON — with one deliberate difference between them: jeffrey-persons-direct-serde-cpu.jfr.lz4 – the optimized path. JSON is serialized directly to and from Java objects, with additional caching in place.jeffrey-persons-dom-serde-cpu.jfr.lz4 – the unoptimized path. JSON is routed through a DOM representation (JsonNode) before being converted to Java objects, intentionally creating extra garbage along the way. Because both recordings exercise the same endpoint under the same workload, they make an ideal before-and-after pair for generating flamegraphs and differential graphs, as we show later. Exploring the Primary Flamegraphs Let's start with the optimized recording. Click jeffrey-persons-direct-serde-cpu.jfr.lz4 to open its profile, then head to the Visualization tab and select Primary under Flamegraphs in the sidebar. Jeffrey inspects the recording and presents a card for every flamegraphable event type it found — each ready to render on its own: Execution Samples (jdk.ExecutionSample) – CPU profiling via perf_events, the most relevant card for a CPU profile like this one.Wall-Clock Samples (profiler.WallClockSample) – wall-clock time, including waiting.Allocation Samples (jdk.ObjectAllocationInNewTLAB) – memory allocation, weighted by object count or total bytes.Java Monitor Blocked, Java Thread Park, Java Monitor Wait – lock-contention and thread-parking events. Each card shows the event type, its source (Async-Profiler or the JDK), the sample count, and a few rendering options — for example, Use Thread-mode to split the graph by thread, or Use Total Allocation to weight the allocation flamegraph by bytes rather than sample count. Click View Flamegraph on the Execution Samples card to see where the CPU time goes. Timeseries Above the flamegraph, Jeffrey plots the selected event across the recording's timeline, so you can see how activity changes over the run — warm-up, steady state, and spikes all stand out. Drag the handles on the range selector below to narrow the window, and the flamegraph rebuilds from only the samples in that interval. Flamegraph Each box is a stack frame, its width proportional to the samples that captured it, stacking upward toward the methods running on-CPU. Wide boxes are where time goes. Read top to bottom to follow the full call path from entry point down into your own code. Click any frame to zoom into that subtree. Search The search box highlights every frame matching your query and reports what share of the profile those matches account for — a fast way to answer "how much time is really in my code?" and to locate a method however deep it sits. The Frame Tooltip Hovering a frame shows far more than a sample count: total vs self samples (time through the frame vs directly in it), its bytecode index and source line, and a compilation breakdown — JIT-compiled, C1-compiled, or inlined — revealing how the method was actually executed. Open in IDE, and View Source jump straight to the code, once Microscope is paired with the Jeffrey IntelliJ plugin. Copy for AI The Copy for AI button exports the current view — stacks, weights, and hot paths — as a compact Markdown summary, copied to your clipboard or downloaded as .md. Paste it into e.g. Claude Code and let the AI optimize your code based on runtime profiles from flamegraphs. Other Flamegraphs Everything above applies to more than just CPU. Back on the Primary page, you can open the Allocation and Wall-Clock flamegraphs the same way — same navigation, search, tooltip, and range selector — but each answers a different question: Wall-Clock – where wall-clock time is spent, including waiting, rather than just on-CPU work.Allocation – where memory is allocated. Two rendering options are worth trying: Use Thread-mode – splits the graph by thread, showing per-thread call trees instead of one merged view. Handy when a single thread dominates or misbehaves. Use Total Allocation – switches the allocation graph from sample count to weight: each frame is sized by the number of bytes allocated rather than how many samples hit it, so a rarely-sampled path that allocates large objects shows up at its true cost. Weighting by the event's own measure instead of sample count often paints a very different — and more actionable — picture. Summary In this article, we set up Jeffrey Microscope and walked through reading a flamegraph — the timeseries and range selector, search, the frame tooltip, the Copy for AI export, and the allocation and wall-clock variants. That's already enough to find where an application spends its time and to start optimizing with confidence. Thank you for reading! To go deeper, visit the Jeffrey pages, or reach out to me directly on LinkedIn — I'd love to hear your feedback. And stay tuned: in the next article, we'll put these two recordings side by side and show how Jeffrey's Differential flamegraph pinpoints exactly what changed between the optimized and unoptimized code.
Codename One has run on the desktop for a long time through the JavaSE target, which is the same engine that powers the simulator. What it did not have was a real native Mac binary, and the desktop output still carried a lot of phone-shaped habits: a drawn toolbar where the OS menu bar belongs, scrollbars you could not grab, no place in the menu for Preferences or Quit. With version 7.0.250, we finally have an actual native macOS application target that doesn't bundle a JVM and is as native as our iOS target. A Native Mac Build From the iOS Pipeline PR #5053 adds a Mac Native target that takes the existing project through the same build as the iPhone builder and the ParparVM pipeline that produces an iOS app. In this case, it emits a native Mac variant of it. We can find these targets in the standard Maven menu in IntelliJ as "Mac Native Build" to send a cloud build: Or as "Mac Native Project" to generate an Xcode project: These targets should work in the same way as the equivalent iOS targets. Thanks to our switch to Metal, the code for the native Mac build is very similar. That means the code of the Mac native target is mostly battle-tested. We use Mac Catalyst, which is an iOS/Mac porting framework from Apple. The user-facing name is "Mac native," and a future phase might add an AppKit target sharing the same Metal renderer without changing the surface you build against. One thing to keep in mind is that the iOS native interfaces would be the same for the desktop target; this might work out fine, but in case it doesn't, you can use #ifdef to adapt code for the Mac target. Here is a Codename One sample running as a native Mac app, the same Java code that produces the iOS and Android builds (it uses the new advertising API covered later this week): Certificates There's one major gap with the Mac target: signing. Right now our certificate wizard, settings, etc. are geared towards iOS/Android. Mac uses a different store and different signing tools. We didn't update all of that infrastructure yet, and it might take some time to update. As a short-term solution, we support some build hints to configure this: HintPurposecodename1.mac.appidMac bundle identifier (the App Store Connect record is distinct from the iOS one).codename1.mac.certificatePath to the .p12 containing the Mac signing certificate. Bundle both Mac App Distribution and Developer ID Application into a single P12 when targeting both channels.codename1.mac.certificatePasswordPassword to unlock the P12.codename1.mac.provisionPath to the Mac .provisionprofile. Desktop Integration PR #5136 and the follow-up PR #5170 make a desktop target behave like a desktop app rather than a tablet app in a window. Everything here is opt-in, on by default for newly generated apps, and completely inert on mobile or when disabled. It spans the core plus desktop ports, JavaSE, Mac, and future ports. Window Chrome and the OS Title Bar A new build hint chooses how the window is framed: Properties files desktop.titleBar=native In native mode, the Codename One Toolbar is suppressed, the form title goes to the OS title bar, and your commands are bridged to a real native menu bar (a Swing JMenuBar that becomes the macOS screen menu on JavaSE, a UIMenuBuilder menu on Mac Catalyst). custom gives you an undecorated window with Codename One drawn caption buttons and window drag; toolbar keeps the classic behavior. Together these modes let you control how the app looks in a deeply customized way. Commands Land in the Right Menu Instead of every command piling into one synthetic menu, a command can declare where it belongs: Java Command prefs = Command.create("Preferences...", null, e -> showPreferences()); prefs.setDesktopMenu(Command.DESKTOP_MENU_PREFERENCES); prefs.setDesktopShortcut(',', Command.DESKTOP_SHORTCUT_MODIFIER_PRIMARY); Command save = Command.create("Save", null, e -> save()); save.setDesktopMenu(Command.DESKTOP_MENU_FILE); save.setDesktopShortcut('s', Command.DESKTOP_SHORTCUT_MODIFIER_PRIMARY); setDesktopMenu(...) takes any of DESKTOP_MENU_APP, ABOUT, PREFERENCES, QUIT, FILE, EDIT, VIEW, WINDOW, HELP, or a custom top-level title string, so Preferences and Quit show up where a Mac user expects them. setDesktopShortcut(...) attaches a keyboard accelerator; DESKTOP_SHORTCUT_MODIFIER_PRIMARY is Command on macOS and Control elsewhere, so the same code does the right thing on each desktop. The accelerator both appears next to the menu item and fires from the keyboard. Interactive Scrollbars Desktop scrollbars are now grab-and-drag with a draggable thumb, click-track paging, and an always-visible track, following the macOS and Material conventions. The thumb shows its hover style under the pointer and its pressed style while dragged, and a minimum thumb size keeps it grabbable on very long content. This is gated by the interactiveScrollBool theme constant and uses dedicated Desktop* UIIDs, so mobile styling is untouched. Desktop Notifications PR #5170 makes the standard LocalNotification API work on a real desktop build, not just in the simulator. On JavaSE, a scheduled notification surfaces through a persistent system-tray icon as a native OS notification, and clicking it dispatches to your LocalNotificationCallback on the same code path mobile uses. Mac Catalyst keeps using the iOS notification path. The same notification code you already wrote for mobile now runs on the desktop. Generated Apps Get This for Free New projects from the archetype and the Initializr default to desktop.titleBar=native with interactive scrollbars on, and the modern themes ship the Desktop* and Window* UIIDs in light and dark (macOS conventions in ios-modern, Material in android-material). If you have an existing app, opt in with the two hints above and check the new UIIDs against your theme. This was validated end to end on both desktop builds: the JavaSE fat jar and the Mac Catalyst .app were each driven through the same AppleScript robot test for window title, menu placement, and native-menu command firing. The full Desktop Integration chapter in the developer guide covers the details. The release post has the full week's index. Tomorrow's deep dive covers WebSockets, gRPC, and GraphQL in the core, the same theme of giving a Codename One app better ways to talk to the outside world.
Although Java 26 was released in mid-March this year, Java 25 is the latest LTS version available, and thus I chose to focus my attention on it in the first place. Irrespective of whether certain Java 25 language improvements are still available as preview features or not, this article briefly outlines a few. The main purpose is to first make the developers aware that Java is continuously refined and evolved by its API contributors and secondly, to raise the curiosity and interest of exploring these enhancements in detail. Out of the bunch of features proposed in JDK 25 [Resource 1], the following five language enhancements are briefly explored here: JEP 512 – Compact source files and instance main methodsJEP 513 – Flexible Constructor BodiesJEP 507 – Primitive Types in Patterns, instanceof and switchJEP 506 – Scoped ValuesJEP 502 – Stable Values Compact Source Files and Instance Main Methods (JEP 512) After its initial proposal as part of JDK 21 as JEP 445 – ‘Unnamed Classes and Instance main Methods', this feature has been gradually improved in the next releases based on the feedback received, and it was finalized in JDK 25. The goal is clear – Simplify Java’s entry point for beginner developers and in small programs — reducing boilerplate and ceremony — while remaining fully compatible with the standard Java language and toolchain. Let’s imagine we quickly want to write a small program that: prompts the user and keeps reading their input in a loopif the user types exit (case-insensitive), it prints “Goodbye!” and endsotherwise, it prints the length of the entered string The code for this resides directly in a package, in a file called CompactSourceFile.java file, whose content looks as below: Java static final String EXIT = "exit"; String prompt(String exit) { return "Enter a string (or '" + exit + "' to quit): "; } void main() { while (true) { String input = IO.readln(prompt(EXIT)); if (EXIT.equalsIgnoreCase(input)) { IO.println("Goodbye!"); break; } IO.println("Length: " + input.length()); } } Suggestive and to the point — no class declaration, just the aimed simple piece of code. If run and after providing a few prompts, the output is as expected: Plain Text Enter a string (or 'exit' to quit): joke Length: 4 Enter a string (or 'exit' to quit): meeting Length: 7 Enter a string (or 'exit' to quit): exit Goodbye! A few observations are worth making: The need for an explicit class declaration is removedAlthough not visible, the compiler implicitly declares a class that is final and part of an unnamed packageThe traditional public static void main(String[] args) is replaced with a simpler enough instance method that is a clearly defined program entry pointThe program entry-point still needs to be named main() as the JVM looks for such a launchable methodAll fields and methods belong to the implicit class, just as in the regular caseThe simple program focuses directly on its purpose without additional detailsIt’s experimental; it’s straightforward. If it turns into a real application though, it’s advisable to preserve the object-oriented structure and all known best practices Flexible Constructor Bodies (JEP 513) Until JDK 25, one clear rule regarding constructors was that no statements could be written before super() or this() calls. For the sake of expressivity and readability, JEP 513 relaxes this constraint, while the existing code continues to compile and function correctly, and moreover, the object’s safety is 100% preserved. In Java, when an object instance is constructed, there are two stages that happen, one before and one after; the hierarchy of constructor chaining begins its execution. During the former, the memory is allocated and the instance fields are initialized, then during the latter, once the this() and super() calls complete, the rest of the object is basically constructed. This process is mainly a safety-wise one, that is to ensure the inherited object parts are completely initialized before any child-related code is run. Joshua Bloch has already advised in his ‘Effective Java’ book to prevent this reference to escape “too early.” The result – objects are not partially constructed at any moment. Simply put, starting with Java 25, statements are now allowed to be executed before this() or super() as part of constructor bodies and still, internally without making any compromises in regard to object core safety while building it. Observations: Allowed statements – only those that don’t depend on instance state and are guaranteed to be safe: manipulation of locally declared variables that live on the stackconstructor parameter validationSyntax is made more permissive, the object safety is preserved Let’s have a small example where we minimally model a Car through an approximate length and the number of wheels, where the former is inherited from a Vehicle super class. Java static class Vehicle { private final long length; Vehicle(long length) { if (length < 0) { throw new IllegalArgumentException("Length must be positive"); } this.length = length; } Vehicle(double length) { long round = Math.round(length); this(round); } public long length() { return length; } } static class Car extends Vehicle { private final int wheels; Car(double length, int wheels) { if (wheels < 0) { throw new IllegalArgumentException("Wheels must be positive"); } super(length); this.wheels = wheels; } public int wheels() { return wheels; } } void main() { var car = new Car(4.6d, 4); IO.println("Car is about " + car.length() + " meters long and has " + car.wheels() + " wheels."); } If we run it, the following output is observed — Car is about 5 meters long and has 4 wheels. First, one may observe that the Vehicle#length is first rounded as it's kept as a long value (line 13) then passed to the other constructor. Secondly, the number of wheels is validated before the super constructor is invoked (line 30), then set. Let’s now model a motorcycle using records. Java record Moto(long length, int wheels) { Moto { if (length < 0) { throw new IllegalArgumentException("Length must be positive"); } if (wheels < 0) { throw new IllegalArgumentException("Wheels must be positive"); } } Moto(double length, int wheels) { long round = Math.round(length); this(round, wheels); } } void main() { var moto1 = new Moto(3, 2); IO.println("Moto 1 is about " + moto1.length() + " meters long and has " + moto1.wheels() + " wheels."); var moto2 = new Moto(2.1d, 2); IO.println("Moto 2 is about " + moto2.length() + " meters long and has " + moto2.wheels() + " wheels."); } While before Java 25, the parameters’ validation is allowed in canonical record constructors (line 2), the ability is now extended for non-canonical constructors as well (line 12), and moreover the this() call is allowed. If we run it, moto1 is constructed using only the canonical constructor, while moto2 via both and the output is obviously the one below. Plain Text Moto 1 is about 3 meters long and has 2 wheels. Moto 2 is about 2 meters long and has 2 wheels. Regarding enums, let’s consider the following experimental code. Java enum Bike { CITY(12), MOUNTAIN("10"); private final int weight; Bike(int weight) { if (weight < 0) { throw new IllegalArgumentException("Weight must be positive"); } this.weight = weight; } Bike(String description) { int weight = Integer.parseInt(description); this(weight); } public int weight() { return weight; } } void main() { IO.println("Bike is " + Bike.MOUNTAIN.weight() + " kg heavy."); } While validation as in the first constructor has been allowed prior to Java 25, additional operations before calling this() are now permitted as well. To conclude, at class, record or enum level, the way the constructors can now be written is cleaned and improved, while the object safety is still preserved without any compromises. Primitive Types in Patterns, instanceof and switch (JEP 507) In general, pattern matching is a language procedure that basically combines a few steps into a feature that facilitates testing a particular value. The focus is on what is being checked and not necessarily on the means of doing it. In addition to situations where pattern matching is applied in case of instanceof and switch constructs, Java 25 allows using it with primitives — byte, short, int, long, float, double, char, boolean are now part of this model. The reference type boundary is now extended, making the feature uniform and more intuitive as the applicability restrictions have been reduced significantly. Let’s consider the following examples: Java void main() { Number doubleBoxed = 3.99; if (doubleBoxed instanceof int i) { IO.println("'num' fits in int: " + i); } else { IO.println("'num' does NOT fit losslessly in int (value=" + doubleBoxed + ")"); } IO.println(describe(Byte.MAX_VALUE)); IO.println(describe(Short.MAX_VALUE)); IO.println(describe(42)); IO.println(describe(Integer.MAX_VALUE)); IO.println(describe(Long.MAX_VALUE)); IO.println(describe(3.14f)); IO.println(describe(2.718281828459045)); } static String describe(Number n) { return switch (n) { case byte b -> n + " fits in byte → " + b; case short s -> n + " fits in short → " + s; case int i -> n + " fits in int → " + i; case long l -> n + " fits in long → " + l; case float f -> n + " fits in float → " + f; case double d -> n + " fits in double → " + d; case null, default -> n + " unknown numeric type"; }; } If run, it produces the below output: Plain Text 'num' does NOT fit losslessly in int (value=3.99) 127 fits in byte → 127 32767 fits in short → 32767 42 fits in int → 42 2147483647 fits in int → 2147483647 9223372036854775807 fits in long → 9223372036854775807 3.14 fits in float → 3.14 2.718281828459045 fits in double → 2.718281828459045 Observations: describe() allows to easily describe a Number as the most compact type it fits into (line 19)A Number reference can now be pattern-matched directly to a primitive (line 20)The feature enables safe, lossless narrowing checks without manual casting or range checks Going deeper with the exploration, what I personally find interesting regarding this feature is the deep nested patterns. The below example allows introspecting the object and directly matching the content. Java record Age(int years) {} record Wine(String name, Age age) {} void analyze(Object value) { IO.println("Analyzing - " + value); if (value instanceof Wine(String name, Age(int years))) { IO.println("Wine: " + name + " (" + years + " years old)"); } else { IO.println("Not a wine"); } } void main() { var value1 = new Wine("Merlot", new Age(10)); analyze(value1); var value2 = "Cabernet Sauvignon"; analyze(value2); } If run, the result is again obvious, but the code is clean, concise, and very expressive. Plain Text Analyzing Wine[name=Merlot, age=Age[years=10]] Wine: Merlot (10 years old) Analyzing Cabernet Sauvignon Not a wine To conclude, beginning with Java 25 in regard to the current state of the pattern matching feature, code has a great chance to become cleaner and safer as a whole. Scoped Values (JEP 506) As Project Loom brought virtual threads in Java, that definitely made room for another enhancement — passing immutable context between and across threads in a more structured, predictable, and safer way. ScopedValues are a finalized feature in Java 25 and allow exactly this, within the boundaries of a precise execution scope. To better understand them, let’s refer to the following simple example: Java static final ScopedValue<User> USER = ScopedValue.newInstance(); record User(int id, String name) {} static void handleFurther() { IO.println("handleFurther - start for " + USER.get()); ScopedValue.where(USER, new User(2, "AD")) .run(() -> { IO.println("handleFurther - something specific for " + USER.get()); }); IO.println("handleFurther - finished for " + USER.get()); } static void handle() { IO.println("handle - start for " + USER.get()); handleFurther(); IO.println("handle - finished for " + USER.get()); } void main() { ScopedValue.where(USER, new User(1, "HCD")) .run(() -> { IO.println("main - before handling - " + USER.get()); handle(); IO.println("main - after handling - " + USER.get()); }); //handle(); } The spot for the shared User is first created as USER. The context passed during the execution (and not as a parameter of the methods engaged) is the User instance. It might be seen as the “current” user. Once the instance is bound (line 23), its scope is clearly defined in the main() method and passed throughout the execution – to handle() and further to handleFurther(). Access is read-only; it cannot be changed. If during the execution flow it is re-set, as in handleFurther(), that is, a new (nested) sub scope is created and once this sub scope ends, the previous outer scope is continued. If run, the code produces the below output which exemplifies even more clearly what has already been stated. Properties files main - before handling - User[id=1, name=HCD] handle - start for User[id=1, name=HCD] handleFurther - start for User[id=1, name=HCD] handleFurther - something specific for User[id=2, name=AD] handleFurther - finished for User[id=1, name=HCD] handle - finished for User[id=1, name=HCD] main - after handling - User[id=1, name=HCD] In case handle() would be called outside the scope (line 30) and the code re-run, a clear exception is thrown upon reaching this point – Exception in thread "main" java.util.NoSuchElementException: ScopedValue not bound. Key points: where(…).run(…) – binds the value for the duration of the lambda, then unbinds it automatically – there’s no need for manual cleanup.Immutable within scope – once bound, it cannot be changed (but can be re-bound in a nested scope).Cheap with virtual threads – no copying, just a reference.Easy to reason about – the value is always what was bound at the top of the current scopeGood alternative to ThreadLocal which has unbounded lifetime, is mutable and pretty hard to reason about, as its value can be changed anywhere in the call stack.Works beautifully with Structured Concurrency (JEP 505) – child tasks automatically share the parent’s scoped values without copying. To conclude, scoped variables contribute a lot to the concurrency cleanness and safety and help prevent issues such as memory leaks or stale data leaking. Stable Values (JEP 502) I see this enhancement as enforcing effective immutability — both at instance and object level. If prior to Java 25 we created an instance, declared it final, initialized it, and documented that it shall remain unchanged, the reality was sometimes different, as some “content” of the instance was still mutable. StableValue feature allows constructing immutable instances by all means so that once initialized, the object content is guaranteed to remain unchanged as well. StableValues are a JVM enhancement that offers a way of achieving thread-safety and deep immutability, an alternative to accomplishing this via combining locks, synchronization, volatile variables and Atomic references. The behavior is thread-safe by design, detail ensured by the JVM’s internal handling of StableValues. Let’s examine the following code: Java static class User { private final StableValue<String> id = StableValue.of(); private final String name; public User(String name) { this.name = name; } public String id() { return id.orElseSet(() -> UUID.randomUUID().toString()); } public String name() { return name; } @Override public String toString() { return name + " (" + id() + ")"; } } private record Task(CountDownLatch latch, Runnable runnable) implements Runnable { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } runnable.run(); } } void main() { var user1 = new User("HCD"); IO.println("Created " + user1); var user2 = new User("Andrei"); IO.println("Created " + user2); IO.println("User's unique identifiers are: " + user1.id() + ", " + user2.id()); } Observations: A User is simply described by two attributes — while the name is provided at construction time, the id represents an internal unique identifier.id is declared as a StableValue and is lazily initialized when the value is read (if in a concurrent context, by the first thread that performs the action) Once initialized, this value is deeply immutable; it cannot be changed and remains as such until the object is destroyed If run, the output is the following: Properties files Created HCD (477a7dc1-c71f-4189-8c58-13994148ff95) Created Andrei (47647539-9cbe-4890-af23-050ee1fe9379) User's unique identifiers are: 477a7dc1-c71f-4189-8c58-13994148ff95, 47647539-9cbe-4890-af23-050ee1fe9379 It’s clear the ids are set when needed, and their values persist whenever read subsequently. One last observation is worth making regarding the User#id attribute — as a StableValue, it’s automatically thread-safe and lock-free. To demonstrate this, let’s run the next piece of code. Java void main() { var user = new User("Concurrent User"); var latch = new CountDownLatch(1); try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) { Future<?> result1 = exec.submit(new Task(latch, () -> IO.println("Task1 - Id: " + user.id() + " at " + System.currentTimeMillis()))); Future<?> result2 = exec.submit(new Task(latch, () -> IO.println("Task2 - Id: " + user.id() + " at " + System.currentTimeMillis()))); Future<?> result3 = exec.submit(new Task(latch, () -> IO.println("Task3 - Id: " + user.id() + " at " + System.currentTimeMillis()))); latch.countDown(); result1.get(); result2.get(); result3.get(); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } } Tasks 1, 2, and 3 are created and set to read the id of the user created in advance, then executed in parallel. The output below demonstrates that, in this particular run, Task 3 sets the id, and then Tasks 1 and 2 use the same value. Plain Text Task3 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123 Task1 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123 Task2 - Id: f7e12b49-5c21-4898-883b-12013824a683 at 1773834965123 StableValue also comes with quite a few higher-level helper methods (function(), intFunction(), list(), map(), supplier()), each of them useful and suitable in various scenarios. Below is an example of how the Singleton pattern could be implemented. Java record User(int id, String name) {} static class UserService { public UserService() { IO.println("UserService created"); } public void register(User user) { IO.println("Registered " + user); } } static UserService getInstance() { return USER_SERVICE_INSTANCE.orElseSet(UserService::new); } private static final StableValue<UserService> USER_SERVICE_INSTANCE = StableValue.of(); void main() { getInstance().register(new User(1, "HCD")); getInstance().register(new User(2, "Andrei")); } The aim is to have a single instance of the UserService that can be used to register users via the designated method. If we run it, the output is the one below, which clearly shows the constructor is called only once. Plain Text UserService created Registered User[id=1, name=HCD] Registered User[id=2, name=Andrei] To conclude, the StableValue enhancement ensures immutability enforced at JVM level – once the value is set, it’s stable and visible to all threads. Conclusions This article briefly covered a few Java 25 language enhancements, hoping that the straight-to-the-point examples presented offer a starting point for further deep-diving into these features. Whether you have already migrated to the latest LTS or not, whether you have started exploring the latest additions and improvements, I consider this worth doing whatsoever. At JavaOne ’26, during one of the opening keynotes, I remarked this quote: “Java is everywhere AI needs to be.” I couldn’t agree more. In a world where apparently everyone is preoccupied with “Accelerated Inference,” let’s remain optimistic about what the future will bring and continue to build and consolidate our Java foundation by exploring the new additions, staying up to date, and gradually embracing them in our personal and professional projects. Resources [1] – JDK 25 [2] – Sample code is available here.
Landing a data engineering role means clearing a gauntlet that no other software discipline has to face all at once: airtight SQL, production-grade Python, data modeling instincts, distributed-compute fluency (Spark, warehouses, ETL), and system design that has to survive real data volume. Generic coding prep barely scratches the surface, and "just grind LeetCode" advice falls apart the moment an interviewer asks you to model a slowly changing dimension or reason about a skewed join. So we did the work. We evaluated the resources data engineers actually use, judged on five things that matter: relevance to the DE interview loop, depth of practice, realism of the questions, feedback quality, and price. Below is the ranked list. A quick note on methodology: this ranking favors resources that target the data engineering loop specifically, not generic algorithm grinding. That bias is intentional, and it is why the order may surprise you. 1. DataDriven.io Most "interview prep" platforms were built for generic SWE roles and bolt on a SQL section as an afterthought. This one was built from the ground up for the data engineering loop. The catchphrase you will hear repeated in DE communities is that DataDriven.io is LeetCode for data engineers, and it fits: instead of inverting binary trees, you are writing window functions against realistic schemas, designing star schemas, debugging an ETL transform, and reasoning about partitioning, all in an in-browser SQL and Python sandbox that runs your query against real data and tells you exactly where it broke. It is also the rare place where the whole product is built for the job rather than adjacent to it, which is why datadriven.io is great for data engineer interview prep specifically: SQL practice that ramps to multi-CTE analytics, a deep set of Python practice problems, plus data modeling, dimensional modeling, PySpark, and system-design tracks, with execution-based feedback and a difficulty curve that reaches the staff-level questions that actually separate offers from rejections. Verdict: The most targeted, realistic data engineering interview practice available today. Earns the top spot. 2. "Cracking the Coding Interview" (the book, by Gayle Laakmann McDowell) A deserved classic, and intentionally a book rather than a website. CTCI is still the best single artifact for understanding how technical interviews are actually structured: how the conversation flows, how to think out loud so the interviewer can follow your reasoning, how to recover when you get stuck, and how to handle the behavioral and negotiation segments that strong candidates routinely fumble. Most people lose offers not because they could not solve the problem but because they could not show their work, and this book is the canonical fix for that. Where it falls short for our purposes is scope. It will not teach you windowed SQL, slowly changing dimensions, or how to design a lakehouse, and its algorithm focus skews toward generalist software roles rather than the data engineering loop. The data structures and big-O chapters are still worth a pass because algorithm screens do show up, but treat them as a refresher, not your main event. Read CTCI once early in your prep to fix your interview mechanics, internalize the communication patterns, then spend the rest of your time on hands-on, domain-specific platforms. Verdict: Essential reading for interview mechanics; not a substitute for domain practice. 3. "Designing Data-Intensive Applications" (the book, by Martin Kleppmann) If CTCI teaches you how to interview, "DDIA" teaches you what a data engineer is actually supposed to know. Replication, partitioning, consistency models, batch versus stream processing, storage engine internals, the failure modes of distributed systems: this is the conceptual backbone of nearly every data engineering system design round. When an interviewer asks why you would choose a log-structured merge tree over a B-tree, or how you would keep two datastores in sync without losing events, the answers live in these pages. It is dense, and it is emphatically not an interview drill book. You will not find practice questions, and you cannot cram it the night before. What it gives you instead is judgment: the candidate who has internalized DDIA answers "how would you design this pipeline" with the calm of someone who has already thought through the tradeoffs, names the failure cases before being prompted, and explains why a choice holds up under real data volume. Read it slowly over weeks, ideally early in your prep, and pair it with a hands-on platform so the concepts attach to actual queries and schemas rather than floating as theory. Verdict: The definitive conceptual reference. Read it slowly, alongside real practice. 4. LeetCode The default destination, and it earns its spot for one practical reason: the Database problem set is sizable, the algorithm catalog is enormous, and the platform's brand means a large share of companies still pull their initial coding screen straight from it. If your target company is known to run a generic algorithm round before the data-specific rounds, you need exposure here, and the sheer volume of problems plus community discussion means you will rarely be surprised by a pattern you have never seen. The catch for data engineers is that LeetCode was built for the algorithm interview, not the DE loop. Its SQL section is genuinely solid but secondary; the questions are puzzle-shaped rather than drawn from real schemas, and you will not find data modeling, ETL design, dimensional modeling, or Spark anywhere on the platform. There is also a real failure mode here: candidates over-invest in LeetCode because it is comfortable and gamified, then walk into a DE loop under-practiced on the things that actually decide it. Use it deliberately to clear the algorithm gate and to keep your raw coding sharp, then move the bulk of your hours to resources that target data engineering directly. Verdict: Necessary for the algorithm screen; thin for the data-engineering-specific rounds. 5. HackerRank HackerRank is where a surprising number of companies host their take-home and timed online assessments, so practicing in its environment carries a payoff most resources cannot offer: you get comfortable with the exact editor, the exact test-case runner, and the exact time-pressure UI you may actually be scored in. For an assessment you cannot retake, that familiarity is worth real points, because fighting an unfamiliar interface while the clock runs is a self-inflicted way to lose. Its SQL and problem-solving tracks are beginner-friendly, well-structured, and free to work through. The ceiling, though, is lower than you want for a senior DE loop. The problems lean academic and self-contained rather than job-realistic, the SQL rarely reaches the messy multi-table analytics that real interviews probe, and there is nothing on modeling, pipelines, or system design. The smart way to use HackerRank is as format rehearsal: run a few timed sets so the assessment environment feels routine, then build your actual depth somewhere that mirrors the work. Do not let a green checkmark on an easy problem set convince you that you are loop-ready. Verdict: Great for getting comfortable with the testing environment; limited depth. 6. SQLZoo A long-running, completely free interactive SQL tutorial that runs entirely in the browser with no signup, no setup, and no paywall. It walks you from SELECT basics through joins, grouping, subqueries, and window functions, with short hands-on exercises after each concept so you are writing real queries from the first lesson rather than just reading about them. For anyone whose SQL has gone rusty, or who learned it informally and has gaps they cannot quite name, it is the most painless way to rebuild muscle memory before stepping up to interview-grade problems. It is a teaching tool, not an interview platform, and you should treat it as exactly that. The problems stay introductory, the datasets are small and tidy, and there is nothing on data modeling, ETL, pipelines, or system design — the parts of the loop that actually separate data engineers from analysts. Its value is as a fast diagnostic and warm-up: work through the sections that feel shaky, confirm your fundamentals are solid, then graduate to harder, execution-based practice against realistic schemas. Linger here too long, and you will plateau well below where a real interview will push you. Verdict: A friendly free SQL primer; foundational rather than interview-level. 7. "Python for Data Analysis" (by Wes McKinney) Written by the creator of pandas, this is the reference for the kind of data-wrangling Python that shows up constantly in DE take-homes and pairing rounds: reshaping, grouping and aggregating, merging on imperfect keys, handling missing values, parsing dates, and cleaning the kind of messy tabular data that never looks like a tidy LeetCode input. Many data engineering interviews quietly assume this fluency, then hand you a notebook and a dirty CSV and watch how you move; if your Python is sharp on algorithms but clumsy on real data manipulation, this book is exactly the gap-closer. It is a library-and-technique book, not interview prep, and it will not touch SQL, data modeling, distributed compute, or system design. There are also no interview questions to grind, which is fine, because its job is to make the tools second nature so that during a timed exercise you are reasoning about the problem instead of fumbling for the right pandas idiom. Read the chapters on data loading, cleaning, and group operations, keep it nearby as a reference, then go apply the techniques in hands-on practice against problems that actually resemble the job. Verdict: The definitive practical Python reference for data work; not a drill book. 8. "Fundamentals of Data Engineering" (the book, by Joe Reis & Matt Housley) Another deliberate book pick, and the best single survey of the modern data engineering lifecycle: generation, ingestion, storage, transformation, and serving, plus the cross-cutting concerns like orchestration, data quality, and governance that interviewers increasingly probe. Where DDIA goes deep on systems internals, this book goes broad on how the pieces fit together into a working data platform, which is precisely the framing you want for the "walk me through how you'd build X" and "what would you consider before choosing this approach" portions of a loop. It is a framework-and-vocabulary book, not a practice book, and that is both its strength and its limit. It will give you the mental model and the shared language to discuss tradeoffs like a practitioner, which makes you sound, accurately, like someone who understands the field. But it contains no exercises, so reading it alone will not build the hands-on skill an interviewer also tests. Use it to organize everything you know into a coherent lifecycle, fill the conceptual gaps, then go write the queries and design the schemas somewhere that gives you real feedback. Verdict: The best lifecycle overview in print; conceptual, not hands-on. 9. Mode SQL Tutorial A free, well-regarded interactive SQL tutorial built by an analytics company, which shows in its framing: it teaches SQL the way analysts and engineers actually use it, oriented around answering real questions from data rather than solving abstract puzzles. It runs in the browser, takes you from the basics through intermediate analytics queries including aggregation and the early window-function territory, and the explanations are unusually clear about why a query is shaped the way it is. For someone shoring up SQL foundations before diving into harder problems, it is one of the cleanest no-cost on-ramps available. Like SQLZoo, it is a tutorial rather than an interview-prep platform, so it stops well short of the difficulty a real DE loop will throw at you, and it covers none of the modeling, pipeline, or system-design ground. It is best read as a companion to a hands-on platform: use Mode to internalize the analytical mindset and clean up your SQL fundamentals, then take that foundation into execution-based practice where the problems are harder, the schemas messier, and the feedback tells you exactly where your query went wrong. Verdict: A clean free SQL on-ramp; foundational rather than interview-level. 10. Pramp/Interviewing.io (mock interviews) Rounding out the list: peer and expert mock interviews. All the solo practice in the world cannot reproduce the specific pressure of explaining your reasoning out loud to a real human while a clock runs and someone is judging you, and that pressure is exactly where otherwise-prepared candidates fall apart. A handful of mock loops surface the weaknesses you cannot see in yourself: the long silences, the jumping to code before clarifying the question, the inability to narrate a tradeoff. Pramp pairs you with peers for free, while Interviewing.io connects you with experienced interviewers, often anonymously, for higher-fidelity feedback. The honest limitation is supply and specificity. Data-engineering-focused interviewers are scarcer than generalist software ones, so depending on availability, you may land in an algorithm or general system-design mock that only partially mirrors a true DE loop. That is still worth doing, because the communication skills, the structure, the clarifying questions, the calm narration, transfer directly regardless of the exact problem. Schedule one or two once your technical prep is underway, treat the feedback as data, and fix the delivery habits well before the interview that counts. Verdict: Best for rehearsing delivery and nerves; DE-specific matches can be hit-or-miss. How to Actually Use This List You do not need all ten. A focused plan beats a scattered one: Build the foundation. Skim CTCI for interview mechanics and start DDIA for concepts.Do the reps where it counts. Spend the bulk of your time on hands-on, DE-shaped practice that maps directly onto what you will be asked (see #1).Patch specific gaps. Use LeetCode for the algorithm screen, SQLZoo or the Mode tutorial to shore up SQL, and a mock interview or two to rehearse out loud. The candidates who get offers are not the ones who consumed the most content. They are the ones who practiced the actual job. Pick the resources that put you closest to it, start today, and write more queries than you read. Good luck with your loop.
Somewhere right now, an engineer is making the case to rewrite a working PHP app in Node, and the pitch includes the word "modern." I have heard a version of this for fifteen years. The app ships. The customers are happy. The code is unfashionable. And somebody wants to tear it down and rebuild it on a stack that looks better on a resume. I have shipped software for more than 20 years, and these days I spend a lot of my time watching AI coding agents write it. So here is a take that is going to sound backward: the thing everyone makes fun of PHP and Laravel for — that they are rigid, opinionated, and boring- is the exact thing that makes coding agents so good at them. When a machine writes a big chunk of your code, the most valuable thing your framework can give you is predictability, not flexibility. And the trendy, flexible stack the rewrite crowd wants is quietly making your AI tooling worse. The Thing That Makes a Stack Feel Modern Makes AI Worse at It A coding agent is a pattern matcher with a context window. It is good at your codebase to the degree that your codebase looks like the millions of others it trained on, and to the degree that it can guess where things go without reading the whole repo first. A bespoke Node service is the opposite of that. Node and Express enforce almost no structure, and that gets sold as a feature. You arrange the project however your team likes. One team puts routes in routes/. Another co-locates them with handlers. A third invents a domain-folder layout from a blog post someone read once. Controllers, services, models, and middleware live wherever this particular team decided. For a senior team, that freedom is genuinely nice. It is also poison for an agent. When you ask the model to add an endpoint, it first has to infer your project's private conventions from whatever it can see, then guess at the rest. Two runs of the same prompt come out different, because there is no canonical answer to "where does this go." The agent burns its effort rebuilding context your layout never standardized, instead of writing the feature. This is not really a Node problem. It is a configuration-over-convention problem, and it shows up anywhere the layout is a per-team decision. Even Django, a real framework with real conventions, leaves you enough rope (models in one file or split across many, your pick of API layer) that the AI output wobbles more than it does in a stricter framework. The more the framework leaves up to you, the more the agent has to guess. Convention Over Configuration Was an AI Strategy Before There Was AI Now open any Laravel project, built by any team, in any country. You already know where everything is. Models in app/Models. Controllers in app/Http/Controllers. Policies in app/Policies. Migrations follow the same timestamped naming every time. This is convention over configuration, the principle Rails made famous, and Laravel built its whole developer experience around. For two decades it was sold as a way to stop bikeshedding and onboard humans faster. It turns out it was an AI strategy the whole time, and nobody knew it yet. When the file always lives in the same place, and the code always follows the same idiom, the model has effectively seen your project a million times before it ever touches it. The structure it is predicting is not your team's private invention. It is the global standard, which is exactly what the model trained on. So the generated code comes out idiomatic, lands in the right directory, and looks the same across two runs of the same prompt. Laravel even ships official AI-assisted-development docs now, plus a tool called Boost that feeds an agent the framework's own conventions. That is the tell. The thing that makes a framework easy for a new human to read — everything is where you would expect — is the same thing that makes it easy for a machine. AI just raised the payoff on being predictable. What This Looks Like When You Actually Ship I am not making this argument in the abstract. I am watching it play out in my own company's products. Our newest product, ProductWave, is built entirely on PHP and Laravel. Not out of nostalgia. We got tired of the JavaScript churn, the dependency hell, the new framework every nine months, the constant re-platforming. Laravel is opinionated in the right places. You get auth, queues, an ORM, scheduling, and a sane directory structure on day one, so you stop arguing with the tooling and start shipping features. The AI part is what made the bet pay off harder than I expected. Because Laravel's conventions are so consistent, the agents we use write noticeably better code in our Laravel apps than in a from-scratch Node service where every team invented its own layout. Same file, same place, every time. So the output is idiomatic instead of improvised, and it holds up across runs. Here is the difference in the terms that actually matter when an agent is writing your code: What the coding agent facesConvention stack (Laravel, Rails)Bespoke stack (hand-rolled Node)Where a new controller goesSame path in every project on earthWherever this team decided, if anyone didStyle of the generated codeMatches the public examples it trained onMatches your house pattern, if one existsTwo runs of the same promptMostly consistentVary run to runContext it must rebuild per repoAlmost none, the structure is the standardMost of it, the layout is privateHow a new engineer (or agent) reads itLike every other projectLike a new language None of this needs the framework to be technically better on every axis. It needs the framework to make the same decision every time, so neither your new hire nor your AI has to wonder. PHP Got Written Off Years Ago. It Is Worth a Second Look. I know the objection, because the rewrite pitch always carries it: PHP is slow, untyped, stuck in 2010. If your last serious PHP experience was a PHP 5.6 codebase, that picture is more than a decade out of date. PHP 8 added a JIT compiler and a real type system. Union types, readonly properties, enums, the match expression, and Fibers for async are all standard now: PHP // PHP 5.6 function process($value) { if (is_int($value) || is_float($value)) { return calculate($value); } } // PHP 8.x function process(int|float $value): float { return calculate($value); } The performance cliche is just as stale. When Tumblr moved its fleet from PHP 5 to PHP 7, the engineering team documented latency dropping by half and CPU load falling at least 50 percent, and PHP 8 kept climbing from there. This is not a dead language. By W3Techs' numbers, it still runs roughly three-quarters of the websites with a known server-side language, and it powers production at the scale of Etsy and Slack. There are good, boring reasons companies still run on PHP. It is unfashionable on Hacker News, which is a very different thing from being dead. The Rewrite Reflex Gets It Backward So why does the rewrite argument keep coming up? Usually it is what I call resume-driven development. The stated reason is "PHP is outdated." The real reason is that an engineer wants the trendy stack on their resume for the next interview. That is rational for the individual and a disaster for the roadmap. I say that as someone who has approved the rewrite and regretted it! Every team I have watched hit this fork landed the same way. The ones that worked said no to the rewrite, modernized the stack they had, and kept shipping customer value. The ones that did not approve it, spent the better part of two years rebuilding what already worked, shipped nothing new in the meantime, and watched competitors eat their lunch. The AI era adds a line to that math the rewrite crowd never accounts for. When you tear down a legible, convention-driven Laravel app and rebuild it as a bespoke service in a flexible stack, you are not just paying the old rewrite tax. You are actively making your codebase harder for the AI tooling you are betting your future speed on. You are trading a structure the model understands for one it has to relearn. You are spending two years to make your own agents worse at their job. That is the opposite of modernization. What You Should Actually Do You do not have to adopt PHP to use any of this. The principle is about convention, not about a language. For greenfield work, bias toward an opinionated framework. Laravel, Rails, and the convention-heavy frameworks in any language give an agent a predictable surface to generate against. The "we will assemble our own stack" instinct feels powerful and quietly costs you AI quality.Modernize the app you have instead of rewriting it. If you are on an old PHP or Laravel version, upgrade it and adopt the conventions fully. You will get more out of your agents from a current, consistent codebase than from a brand-new language, at a fraction of the cost and risk.If you are stuck in a flexible stack, impose convention anyway. Pick a canonical layout, document it, lint for it, and keep it identical across services. The agent cannot read your mind, but it will follow a structure you actually enforce. Most of the AI-quality gap closes the moment the layout stops being a per-team decision.Stop treating "boring" as an insult. Boring means predictable. Predictable means staffable, and now it means legible to a machine too. In an AI shop, that is the competitive choice, not the compromise. The Bottom Line For fifteen years, the knock on Laravel was that it makes your decisions for you. That was always a strange thing to complain about. Now it is the entire advantage, and the agents are the ones cashing it in.
Alvin Lee
Founder,
Out of the Box Development, LLC