Agents, Tools, and MCP: A Mental Model That Actually Helps
12 Factor Framework for Building Secure and Compliant Cloud Applications
Code Review Core Practices
Shipping Production-Grade AI Agents
Large language models (LLMs) are impressive — until they are not. If you ask one about your internal data, your product catalog, or your users' reviews, it will either hallucinate an answer or admit it does not know. The solution most teams reach for is retrieval-augmented generation (RAG). This retrieves relevant data first, injects it into the prompt as context, and lets the model answer from that context rather than from memory. GraphRAG takes this a step further. Instead of retrieving only text chunks, it can use graph relationships to retrieve connected context, following relationships between entities to build richer, more structured context. The result can provide answers grounded in both data and the relationships between that data. In this article, we'll walk through a practical GraphRAG implementation using Spring AI and Neo4j, built on top of a Goodreads book and review dataset. We'll cover the data model, loading the data, setting up the vector index, running the Spring Boot application, and some lessons learned along the way. The full source code is available on GitHub. What We Are Building The application answers natural language queries like "find books with a happy ending" or "something encouraging" by combining two retrieval mechanisms in Neo4j: Vector search – embeds the search phrase via OpenAI and finds semantically similar book reviews using cosine similarity.Graph traversal – follows the WRITTEN_FOR relationship from matched reviews to their associated books, giving the LLM structured book context rather than raw review text. This example uses a simple GraphRAG pattern where vector search identifies relevant reviews and graph traversal expands the retrieved context to connected books. The LLM then summarizes the retrieved books in the context of the original search phrase. The architecture looks like Figure 1. Figure 1. Architecture. Prerequisites Before we start, we will need: Java 21 or laterA Neo4j AuraDB instanceAn OpenAI API key Installing Java If Java is not already installed, the recommended distribution is Temurin from the Adoptium project, available at adoptium.net. Installers are available for Windows, macOS, and Linux. Once installed, verify with: Shell java -version We should see something like openjdk version "21.x.x". The project uses the Maven wrapper, so there is no need to install Maven separately. Setting Up Neo4j AuraDB AuraDB is Neo4j's fully managed cloud database. A free tier is available. Sign up at neo4j.com/product/auradb/.Create a new AuraDB Free instance.When the instance is created, download or note the credentials — the URI, username, and password. Neo4j only shows the password once, so save it somewhere safe.Once the instance is running, open the built-in Query tab and verify connectivity: cypher MATCH (n) RETURN count(n) . This should return 0. We are ready to load data. AuraDB Free includes Awesome Procedures on Cypher (APOC), a utility that provides numerous procedures and functions for data handling. We'll use APOC for the data loading steps. The Data Model The dataset is built around three core node types: Book – 10,000 books from the Goodreads UCSD datasetAuthor – 12,371 authorsReview – 69,791 user reviews, each linked to a book via a WRITTEN_FOR relationship There is also a User node (44,827 users) linked to reviews via a PUBLISHED relationship, although the main application focuses on Books and Reviews. The graph model is shown in Figure 2. Figure 2. The Goodreads Dataset. The key insight is that the Review node carries two things: the review text and 1,536-dimension embeddings generated using an OpenAI embedding model. This is what makes vector similarity search possible without a separate vector database — Neo4j handles both the graph and the vectors. The Goodreads data used in this article is derived from the UCSD Book Graph dataset and related Goodreads datasets released by researchers at the University of California, San Diego, including Mengting Wan, Julian McAuley, and collaborators. The data is provided for research and educational purposes. If you use these datasets in your own work, please cite the following publications: Mengting Wan and Julian McAuley, Item Recommendation on Monotonic Behavior Chains, RecSys 2018.Mengting Wan, Rishabh Misra, Ndapa Nakashole, and Julian McAuley, Fine-Grained Spoiler Detection from Large-Scale Review Corpora, ACL 2019. Loading the Data Let's load the data step by step in the AuraDB Query tab. Run each of the following blocks separately. Constraints and Indexes First, let's set up the constraints and the vector index: Cypher CREATE CONSTRAINT FOR (b:Book) REQUIRE b.book_id IS UNIQUE; CREATE CONSTRAINT FOR (a:Author) REQUIRE a.author_id IS UNIQUE; CREATE CONSTRAINT FOR (r:Review) REQUIRE r.id IS UNIQUE; CREATE CONSTRAINT FOR (u:User) REQUIRE u.user_id IS UNIQUE; CREATE INDEX FOR (r:Review) ON (r.user_id); Then create the vector index on the Review node's embedding property: Cypher CREATE VECTOR INDEX `review-text` IF NOT EXISTS FOR (n:Review) ON (n.embedding) OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' }; Note the index name review-text — we will come back to this in the lessons learned section. Loading Books and Authors The data are hosted on Neo4j's public servers, so we can load them directly via APOC: Cypher CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_books_10k.json") YIELD value as book MERGE (b:Book {book_id: book.book_id}) SET b += apoc.map.clean(book, ['authors','similar_books'],[""]); Next, we'll load the initial author stubs: Cypher CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_books_10k.json") YIELD value as book WITH book UNWIND book.authors as author MERGE (a:Author {author_id: author.author_id}); and then populate the author nodes with the full data: Cypher CALL apoc.periodic.iterate( 'CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_book_authors.json.gz") YIELD value as author', 'WITH author MATCH (a:Author {author_id: author.author_id}) SET a += apoc.map.clean(author, [],[""])', {batchsize: 10000} ); Next, we'll create the AUTHORED and SIMILAR_TO relationships: Cypher CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_books_10k.json") YIELD value as book WITH book MATCH (b:Book {book_id: book.book_id}) WITH book, b UNWIND book.authors as author MATCH (a:Author {author_id: author.author_id}) MERGE (a)-[w:AUTHORED]->(b); Cypher CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_books_10k.json") YIELD value as book WITH book MATCH (b:Book {book_id: book.book_id}) WITH book, b WHERE book.similar_books IS NOT NULL UNWIND book.similar_books as similarBookId MATCH (b2:Book {book_id: similarBookId}) MERGE (b)-[r:SIMILAR_TO]->(b2); Loading Reviews This step can take several minutes, as it is pulling and processing approximately 70,000 reviews from a gzipped JSON file: Cypher CALL apoc.load.json("https://data.neo4j.com/goodreads/goodreads_reviews_dedup.json.gz") YIELD value as review CALL { WITH review MATCH (b:Book) WHERE b.book_id = review.book_id WITH review, b MERGE (r:Review {id: review.review_id}) SET r += apoc.map.clean(review, [],[""]) WITH b, r MERGE (b)<-[rel:WRITTEN_FOR]-(r) } in transactions of 20000 rows; Note that review.review_id is stored as the Review node's id property, which Spring AI expects when mapping vector search results. Then we'll separate the User nodes from the Review data: Cypher MATCH (r:Review) WHERE r.user_id IS NOT NULL CALL { WITH r MERGE (u:User {user_id: r.user_id}) WITH r, u MERGE (r)<-[:PUBLISHED]-(u) } in transactions of 20000 rows; Adding the text Property Spring AI maps vector search results to Document objects using a property named text. Our review data uses review_text, so we need to add the text property: Cypher MATCH (r:Review) CALL { WITH r SET r.text = r.review_text } IN TRANSACTIONS OF 20000 ROWS; Loading Pre-Generated Embeddings Rather than generating embeddings at runtime, which costs tokens and time, we'll load pre-computed embeddings hosted by Neo4j. This step also takes several minutes: Cypher LOAD CSV WITH HEADERS FROM "https://data.neo4j.com/goodreads/review_embeddings.psv" as row FIELDTERMINATOR '|' CALL { WITH row MATCH (r:Review {id: row.reviewId}) CALL db.create.setNodeVectorProperty(r, 'embedding', apoc.convert.fromJsonList(row.embedding)) RETURN r } in transactions of 1000 rows WITH r RETURN count(r); Once complete, we can verify the embeddings loaded correctly: Cypher MATCH (r:Review) WHERE r.embedding IS NOT NULL RETURN count(r) AS reviews_with_embeddings We should see 69791. Exploring the Data Before running the application, let's take a look at what we have loaded. Here are a few useful queries to run in the AuraDB Query tab. Browse the top-rated books: Cypher MATCH (b:Book) RETURN b.title, b.average_rating ORDER BY b.average_rating DESC LIMIT 10 Browse books with their authors: Cypher MATCH (a:Author)-[:AUTHORED]->(b:Book) RETURN a.name, b.title, b.average_rating ORDER BY b.average_rating DESC LIMIT 10 Inspect a sample embedding — we can see the first few dimensions of a review's vector: Cypher MATCH (r:Review) WHERE r.embedding IS NOT NULL RETURN r.id, r.text, r.embedding[0..5] AS embedding_sample LIMIT 5 Building and Running the Application Let's clone the GitHub repo and get the application running: Shell git clone https://github.com/JMHReif/springai-goodreads.git cd springai-goodreads Set the environment variables for Neo4j AuraDB and OpenAI, as follows: Shell export SPRING_NEO4J_URI=neo4j+s://xxxx.databases.neo4j.io export SPRING_NEO4J_AUTHENTICATION_USERNAME=your_username_here export SPRING_NEO4J_AUTHENTICATION_PASSWORD=your_password_here export SPRING_AI_OPENAI_API_KEY=your_openai_key_here These variables must be set in the terminal session used to run the Spring Boot application, specifically the window where you run ./mvnw spring-boot:run. The terminal used for curl commands does not need them. To avoid having to re-export them each time, you can add them to your shell profile (e.g. ~/.zshrc on macOS or ~/.bashrc on Linux) or save them in a small shell script and source it before starting the app. Now we'll start the application from the root of the cloned repo, where the pom.xml and mvnw files live, as follows: Shell ./mvnw spring-boot:run Maven will download dependencies on the first run. Once the startup banner appears, the app is ready on port 8080. The Four Endpoints The application exposes four REST endpoints, each representing a different retrieval strategy: /hello — Baseline LLM Call Shell curl "http://localhost:8080/hello" A simple call to the LLM with no retrieval. Useful to verify the OpenAI connection is working. /llm — LLM With No Context Shell curl "http://localhost:8080/llm?searchPhrase=happy%20ending" This sends the search phrase directly to the LLM with no data from Neo4j. The model answers from its training data — fast, but prone to hallucination and not grounded in our Goodreads data. /vector — Vector Search Only Shell curl "http://localhost:8080/vector?searchPhrase=happy%20ending" Spring AI embeds the search phrase via OpenAI, queries the review-text vector index in Neo4j, and passes the matching review text to the LLM. Semantic matching works well here — the phrase does not need to match any exact words in the reviews. /graph — Full GraphRAG Pipeline Shell curl "http://localhost:8080/graph?searchPhrase=happy%20ending" This is the full pipeline. Vector search finds the most semantically similar reviews, the graph traversal follows the WRITTEN_FOR relationship to retrieve the associated Book nodes, and the LLM receives structured book context rather than raw review text. Let's look at the output for a few different search phrases: Shell curl "http://localhost:8080/graph?searchPhrase=encouragement" curl "http://localhost:8080/graph?searchPhrase=high%20tech" curl "http://localhost:8080/graph?searchPhrase=caffeine" The contrast between /llm and /graph on the same phrase is the most compelling comparison — the LLM answers from memory in one case and from our actual Goodreads data in the other. GraphRAG Uses Both Vector Search and Graph Traversal It's worth comparing the two retrieval strategies directly, as shown in Figure 3. Figure 3. Vector Search and Graph Traversal. Neither approach is strictly better. Rather, they are complementary. Vector search handles fuzzy, intent-driven queries that keyword search would miss entirely. Graph traversal adds relationship-aware context that makes the LLM response richer and easier to trace back to source data. The /graph endpoint combines both. Lessons Learned Here are four things worth knowing before setting this up from scratch. Vector index naming matters. Spring AI's default vector index name is spring-ai-document-index. This project requires review-text. If the index is created with the wrong name, the application throws a runtime error that is not immediately obvious. Always check the index name configured in the application against the one created in Neo4j.Review nodes need id and text properties. Spring AI maps vector search results to Document objects using properties named id and text. In this dataset, review_id is mapped to the Review node's id property during loading, but the review text is stored as review_text. We therefore add a text property so Spring AI can map the results correctly. Without the expected properties, vector search returns results, but the book list comes back empty — the model gets no context and answers from memory instead.Pre-generated embeddings save time and money. Generating 69,791 embeddings at runtime via the OpenAI API would be slow and costly. Loading pre-computed embeddings from a file is much faster for initial development setups. The trade-off is that the embeddings are fixed, as they were generated with a specific OpenAI model and will need to be regenerated if the model changes.Data loading takes patience. The two long-running steps are the review load and the embedding load. Plan for this, although both steps only need to be done once and the database can be left running between sessions. Summary GraphRAG is a practical pattern, not just a research concept. By combining Neo4j's graph traversal with its vector index, we get two retrieval mechanisms in a single database, and no separate vector store is required for this architecture. Spring AI provides the abstractions to wire it all together in a way that will feel familiar to any Spring developer. The Goodreads domain is approachable and familiar to many readers, but the architecture generalizes to any graph of connected entities, such as product catalogs, knowledge graphs, and collections of documents. If you have relationships in your data, a graph database gives you relationship-aware retrieval capabilities that a plain vector store does not provide. The full source code is on GitHub. Acknowledgements I thank my colleague Jennifer Reif for sharing the Spring AI example.
In a world where testing is mainly test execution, it is reasonable to expect that when people test, they simply expect to find bugs. "We test to find bugs" is the answer given in job interviews. An answer repeated in onboarding materials, embedded in KPI frameworks, and implicitly assumed in every conversation about when software is ready to ship. It is so thoroughly taken for granted that questioning it sounds absurd, like questioning whether hammers are for hitting things. Testing does find bugs since it involves test execution. But testing involves much more than execution. Framing testing as just a bug-finding activity results in a number of consequences that this article will discuss. The History To understand why the bug-finding framing is so persistent, it helps to understand where it came from. It did not emerge from careful thinking about what testing is for. It emerged from the early days of software development. In the sequential, phase-gated models of software development that dominated from the 1960s through the 1990s, testing was a phase. It came after coding. Its purpose was to check whether what had been built worked as specified. The people who did it were called testers, and the outputs of their work were bug reports. The entire apparatus — the phase, the role, the output — was organized around the assumption that defects were artifacts to be found and removed, like impurities in a manufactured component. This was a coherent model for its time and context. It mapped reasonably well onto hardware-adjacent software development. Change was expensive, requirements were relatively stable, and the cost of late discovery — while significant — was at least bounded by the pace of development cycles measured in months or years. Then software development changed. It was all about speed of development. Cycles accelerated. Requirements destabilized. Systems grew interconnected. The gap between what could be specified in advance and what users actually needed widened dramatically. The contexts in which the bug-finding model made sense dissolved — but the model remained. It was too embedded in tooling, organizational structures, hiring practices, and professional identity to dissolve with them. What remained was a model designed for a slow, sequential world, applied to a fast, iterative one. The phase became a sprint ceremony. The tester became a QA engineer. The bug report became a Jira ticket. The vocabulary updated. The underlying assumption, that testing is what you do to find defects in something already built, did not change, however. Bug Finders in the SDLC A bug may exist anywhere in the SDLC. A missing or ambiguous requirement. A design that fails to handle important scenarios. Incorrect or incomplete code. A flawed or missing test that allows defects to escape. A deployment or configuration error that causes the software to behave differently in production. So, in theory, bug finders could be employed all around the SDLC. But in practice, when testing is framed as bug finding it often becomes a late-stage filter. This determines what teams hire for, how they structure work, what they measure, how they explain incidents, and where they invest. It's not a minor detail! When organizations test to find bugs, features are typically designed, developed, and code-reviewed. A hand-off model is employed whereby, when development is complete, the feature moves to the QA team. The QA team writes test cases based on the requirements document. They execute the tests. They find several bugs. The bugs are returned to the developers. The developers fix the bugs. The feature returns to QA for regression testing. It passes. It ships. Under such settings, bugs are often exposed late, during the testing process after development. When information about a defect arrives late, decisions have already been made on the assumption that the defect does not exist. Those decisions must be revisited, revised, or lived with. The longer the latency, the more decisions have accumulated on a false foundation. Hiring Bug Finders One of the most direct and least-discussed consequences of the bug-finding framing is what it does to hiring. When an organization believes testing is about finding bugs, a good tester is a good bug-finder. But bug-finding, as a job description, selects for a specific and narrow set of skills. The typical bug-finding hire is technically literate but not technically deep. Comfortable at designing/executing test cases, skilled at documenting defects clearly, experienced with the tooling of defect tracking. These are real and useful skills, but they are a small snapshot of the value that testing can produce. Testing requires the capacity to ask questions that have not been asked before. It requires understanding of system architecture, since you cannot generate useful information about a system that you do not understand. It requires risk reasoning — the ability to identify which regions of the system's behavior space carry the most consequence if they are wrong. It requires communication skills oriented not toward defect documentation but toward translating. Translating technical evidence into decisions that technical and non-technical stakeholders can act on. It requires curiosity: a genuine interest in what the system is actually doing, rather than in whether it matches an expected outcome. The Hiring Mirror Job descriptions reveal the framing. "Find and report defects" → bug-finding model. "Generate evidence about system behavior and communicate risk" → information-generation model. Most job descriptions in the industry describe the first. Most organizations need the second. The gap between them is filled, imperfectly, by individuals who developed the second set of skills despite a system that did not ask for them. Team Structures Information about software behavior is generated — or should be generated — all around the SDLC. By developers writing unit and integration tests. By product owners reviewing acceptance criteria. By architects thinking through failure modes. By security engineers probing for vulnerabilities. By data analysts examining production telemetry. The bug-finding framing implicitly assigns all of this to testers. The team structures produced are almost universally recognizable, because they follow directly from the assumption that testing is a downstream, verification activity. This has major implications. The Bottleneck Problem When QA is the terminal stage before release, it becomes a bottleneck by construction. Development velocity is limited not by the rate at which developers can produce code but by the rate at which QA can process it. Organizations respond to this by automating testing and/or hiring more QA engineers, which increases throughput without addressing the underlying problem. In some cases, organizations have also reduced the time allocated to testing. This reduces thoroughness without anyone explicitly deciding to accept that risk. The Ownership Problem So, developers write code and testers verify that the code works OK. Does this imply that QA owns quality? Organizations that answer yes to this question open the door to other dangers. If testers own quality, then developers have a reduced incentive to ensure the correctness of their own work. After all, it's someone else's job. The empirical result is predictable and well-documented: defect rates increase when developers operate in an environment where defect detection is handled downstream. The Adversarial Problem In organizations where the hand-off model is combined with metrics that reward development velocity and measure QA performance by defect counts, a perverse incentive structure emerges. Development is rewarded for speed; QA is rewarded for finding bugs. The implicit incentives push these functions into an adversarial relationship — developers resenting QA for slowing release, QA resenting developers for producing buggy code. This adversarial dynamic is not a cultural problem. It is an incentive problem produced by a framing error. structurewhat it reveals about the framing Separate Dev and QA teams with hand-off Testing is a distinct downstream activity. Quality is owned by QA QA measured by bugs found Success is defined as defect detection, not information generation 'QA sign-off' as a release gate Testing is a filter, not a continuous discipline Testers assigned to features after development Testing is verification of completed work, not a parallel information stream Embedded QA in cross-functional teams Testing is a continuous contribution to the development process Shared Definition of Done including test evidence Quality is a property of the team's output, not a downstream check Developers writing and owning test suites Information generation is distributed to the point of production The Release Process The release process is where the framing becomes most visible. This is because the release process is the moment at which the organization is forced to answer the question: do we know enough to ship? The answer to that question is operationalized as: have we found enough bugs? The proxies are test execution rates, pass/fail ratios, open defect counts, and severity distributions. A release is approved when the open defect count is below a threshold, the regression suite passes, and the severity of known issues is judged acceptable. The assumption embedded in this process is that the tests have found the bugs, the bugs have been fixed, and what remains is known and manageable. This assumption, however, is almost never fully warranted. Experienced release managers know it. Every release approval is accompanied by a degree of unspoken uncertainty — a sense that the tests have probed what they have probed, and that what they have not probed remains unknown. This uncertainty is rarely made explicit, because the framework does not have a vocabulary for it. The question "what do we not know about this system's behavior?" often has no formal place in a release process organized around defect counts. The question is not "have we found the bugs?" It is "have we generated sufficient evidence about the risks that matter most, and is the residual risk in the untested regions acceptable given what we know about this system's usage and consequences?" This is a harder question to answer, but it is the right question. It produces better decisions since it forces the organization to name its assumptions rather than hide them under a metric. Incident Post-Mortems When something breaks in production, the organization is forced to explain how it happened. Post-mortems tend to converge on a specific narrative: the defect was present but was not found by testing. The conclusions that follow are predictable — more tests, better coverage, improved test cases for this class of defect. Sometimes a retrospective guilt is attached to a specific test or test type that should have caught the issue. Such a narrative can be dangerous for the cohesion of teams. It is also incomplete. It asks what the filter missed, when the right question is why the filter was the primary defense against this class of failure. It treats the absence of a specific test as the root cause. However, the actual root cause may be that the organization's testing model was structurally incapable of generating the information that would have prevented the incident. To put simply, why does testing only follow development? A few legitimate questions for post-mortems are: What information did we have about this region of the system's behavior before the incident?Why was that information insufficient — was it absent, was it present but not acted on, or was it structurally impossible to generate with the testing approach we were using?At what point in the SDLC was information about this failure mode accessible, and why did it not reach the people who could have acted on it?What does this incident tell us about the shape of our evidence — where are the gaps in what we know about our system's behavior? These are different questions from "which test should have caught this." They produce different answers and different remediation paths. And they are more honest, because they acknowledge that the incident may not always be a failure of test execution. It was a failure of information generation at some point in the SDLC, and the post-mortem's job is to find that point. The Blame Allocation Problem When testers are bug-finders, there can be an uncomfortable dimension to post-mortems that deserves naming directly. When an incident occurs, the implicit question "who was supposed to find this bug?" resolves to the testers. This is expected given the framing. If testing is the activity by which bugs are found, and this bug was not found, then the people responsible for testing bear some responsibility for the failure. This is counterproductive since post-mortems should be blameless. The blamelessness in post-mortems is more important than how we frame our jobs, but most importantly, the two interact with each other. If you find that blaming is part of your post-mortems, then I suggest first identifying why. Why do you need blame in your post-mortems? As you walk through the path to get rid of it, if framing your jobs stands in the way, then a good idea is to rethink the fundamentals. Fundamentals like: What is quality? How do we develop our code? Why do we test? Who owns quality, and how do we learn in software products/projects? Try to get the wider picture possible. Wrapping Up The belief that testing finds bugs is a consequential framing error in software engineering. Not because it is false — testing does find bugs — but because it is so incomplete that organizing a quality program around it produces systematic, predictable, and expensive failures. Testing is much more than bug finding. Testing generates information about software behavior. That information is the raw material of every quality decision made in the SDLC. The earlier that information is generated, the cheaper it is to act on. The later it arrives, the more decisions have accumulated on a false foundation, and the higher the cost of correction. After all, the answer to questions like "what is testing?" propagates through every dimension of how organizations work: who they hire, how they structure teams, what they measure, how they make release decisions, and how they explain incidents. Changing the framing is not a cosmetic exercise. It is a structural change that requires deliberate action at the level of process, metrics, and professional development. If we frame testing as information generation and try to see the wider picture, for example, as in this article, then things could be different in many ways, as shown below. Dimensionbug-findinginformation-generation Purpose of testing Find and remove defects Generate evidence about system behaviour throughout the SDLC Timing Late-stage filter after development Continuous discipline from requirements to production Ownership Owned by QA Distributed across the team; QA provides depth and system perspective Hiring Defect documentation skills Risk reasoning, system understanding, evidence communication and soft skills Team structure Separate dev/QA with hand-off Embedded, cross-functional, shared quality ownership Release decision Gate based on defect count Risk assessment based on evidence quality and coverage Post-mortem 'Which test missed this?' 'Where did information generation fail in our SDLC?' Success metric Bugs found, pass rate Escape rate, detection stage, risk coverage Cost profile Low visible cost early, high hidden cost late Higher visible cost early, lower total cost overall
Once your multi-agent system (Parts 6-8) is functionally solid, the question that comes up in every enterprise security review is the same: how do you know an agent is only doing what it's authorized to do, over a network path you actually trust, with an audit trail that holds up when someone asks "what did this agent do and why" six months later? This post covers private networking, Entra Agent ID, and audit trail design. Zero-Trust Network Boundary Private Endpoints: Keeping Traffic Off the Public Internet By default, calls from your application to Foundry Models travel over the public internet (encrypted, but publicly routable). For regulated workloads, route through Private Link instead: JSON resource foundryPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-04-01' = { name: 'pe-foundry-project' location: location properties: { subnet: { id: subnetId } privateLinkServiceConnections: [ { name: 'foundry-connection' properties: { privateLinkServiceId: foundryResourceId groupIds: ['account'] } } ] } } Pair this with a Managed VNET at the project level so that outbound calls from your compute (the orchestration layer running the agent code from Part 7) never leave the private network boundary. The failure mode to check for: a dependency library making an unexpected public call (a package doing telemetry callback, for instance) that bypasses your intended network boundary entirely — audit your dependencies' network behavior, not just your own code's. Entra Agent ID: Identity for Autonomous Agents, Not Just Services Traditional managed identity was designed for services calling APIs on a fixed schedule with predictable behavior. Entra Agent ID extends this model specifically for autonomous agents that make independent decisions — the identity model needs to answer not just "is this call authenticated" but "was this agent authorized to take this specific action in this specific context." Python from azure.identity import DefaultAzureCredential class AgentIdentityContext: def __init__(self, agent_id: str, allowed_actions: list[str]): self.agent_id = agent_id self.allowed_actions = allowed_actions self.credential = DefaultAzureCredential() def authorize_action(self, action: str, resource_scope: str) -> bool: if action not in self.allowed_actions: log_authorization_denial(self.agent_id, action, resource_scope) return False token = self.credential.get_token(resource_scope) return token is not None def execute_agent_action(agent_context: AgentIdentityContext, action: str, resource_scope: str, fn): if not agent_context.authorize_action(action, resource_scope): raise PermissionError(f"Agent {agent_context.agent_id} not authorized for {action}") return fn() The key design point: each agent in your multi-agent chain gets its own scoped identity with its own explicit allowed_actions list, rather than all agents sharing one broad service principal. This means a compromised or misbehaving refund agent can't accidentally (or maliciously, if prompt-injected) invoke actions scoped only to the fraud-check agent. Security controlWhat it protects againstEnforced wherePrivate Link / Managed VNETTraffic leaving the trusted network boundaryNetwork layerEntra Agent ID scoped identityAn agent invoking actions outside its roleIdentity/authorization layer, in codeStructured audit log with reasoningInability to explain why an action was takenApplication logging layerAuthorization enforced in code, not model claimsPrompt injection claiming false authorizationApplication logic, never the model's own output Data Residency and Customer-Managed Keys Private networking and identity handle "who can reach this system and act as whom." A separate, equally common enterprise requirement is "where does this data physically live, and who holds the encryption keys" — data residency and customer-managed keys (CMK) address this and get missed by teams who've handled networking and identity but stop there. For data residency, confirm which region your Foundry project, its underlying Azure AI Search index, and any storage backing Foundry IQ actually run in — these can silently default to a different region than your primary application if not explicitly configured, which is a real problem for workloads subject to data-sovereignty requirements (GDPR-adjacent obligations, government contracts with residency clauses, etc.): JSON resource foundryProject 'Microsoft.CognitiveServices/accounts/projects@2024-10-01' = { name: 'your-project' location: 'westeurope' // must match your data residency requirement explicitly — // don't rely on the default location of the parent resource group properties: { // ... } } For customer-managed keys, the default is Microsoft-managed encryption at rest, which is adequate for most workloads but insufficient for some regulated industries that require the ability to revoke access to data by rotating or deleting a key the organization itself controls: JSON resource foundryAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' = { properties: { encryption: { keySource: 'Microsoft.KeyVault' keyVaultProperties: { keyVaultUri: keyVaultUri keyName: 'foundry-cmk' } } } } Neither of these is something you retrofit easily after data has already been written under the default configuration — data residency and CMK are decisions to make explicitly during initial provisioning of the Foundry project, not something to leave as "we'll configure that before the security review." If your compliance requirements are still being finalized when you provision, default to the more restrictive configuration (explicit region pinning, CMK) rather than the platform default, since loosening a restriction later is far easier than migrating already-written data into a stricter configuration retroactively. Audit Trails That Actually Hold Up A log line saying "agent called refund API" is not an audit trail — when someone asks "why did the agent decide to issue this refund," you need the reasoning captured, not just the action: JSON import json import time def log_agent_decision(agent_id, state, decision, reasoning, authorized_by): audit_entry = { "timestamp": time.time(), "agent_id": agent_id, "task_id": state.task_id, "decision": decision, "reasoning_summary": reasoning, # the model's stated rationale, captured verbatim "state_snapshot": { "order_id": state.order_id, "fraud_check_result": state.fraud_check_result, }, "authorized_by": authorized_by, # which identity/policy allowed this "input_hash": hash_conversation_context(state), # for reproducibility without storing PII long-term } write_to_audit_log(audit_entry) Three things a real audit trail needs that a simple action log doesn't: the model's stated reasoning at the decision point (not just the outcome), the authorization context (which identity/policy permitted the action), and enough state snapshot to reconstruct why the decision made sense given the inputs — without necessarily storing full raw PII long-term, which is why hashing the input context is often the right tradeoff between auditability and data minimization. Testing the Security Boundary, Not Just the Happy Path Write tests that deliberately attempt to violate the boundaries you've set up: Attempt to call an action outside an agent's allowed_actions list and confirm it's denied and logged.Attempt a request that would resolve to a public endpoint and confirm the network boundary rejects it.Simulate a prompt-injection attempt that tries to get an agent to claim authorization for an action it doesn't have, and confirm the authorization check (not the model's own claim) is what gates execution. That last point matters most: authorization must be enforced in code against the identity's actual permitted action list, never based on what the model says about its own permissions in its output text. References Private Link for Azure AI Foundry: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-linkManaged VNET for Foundry projects: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-managed-networkMicrosoft Entra Agent ID: https://learn.microsoft.com/en-us/entra/identity/agent-identity/overviewZero Trust architecture guidance: https://learn.microsoft.com/en-us/security/zero-trust/zero-trust-overview
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.
Modern software delivery is complex. Developers are responsible not only for writing code that meets business requirements — both functional and non-functional — but also for navigating a long chain of supporting steps. From containerization, testing, configuration, security, deployment, and monitoring, each stage often relies on specialized tools and teams. When these processes aren’t standardized, every project risks reinventing the wheel. The result is inconsistency, delays, and frustration. For example, requesting a new test environment might require submitting detailed tickets to a DevOps team, slowing timelines and draining energy. As organizations scale, so does the complexity — and the pain of delivery. Platform engineering addresses these challenges by creating shared, reliable foundations. It provides self-service tools, standardized workflows i.e., golden paths, and built-in guardrails, enabling teams to focus on what matters most: writing code and shipping features. This article explores what platform engineering is, why it matters, and how it helps organizations move faster while reducing developer burnout. It also examines common challenges and how to avoid turning platforms into yet another layer of complexity. Platform Engineering Definition Platform engineering is a practice of building and maintaining an internal, self-service platform that makes it easy for development teams to build, deliver, and operate software. Key principles of platform engineering are: Self-service (with guardrails) → developers should be able to build, deploy, and operate services independently — without filing tickets for routine tasks. Also ensuring automated guardrails for compliance and cost control.Golden paths, not golden cages → provide opinionated, well-supported paths that make the right thing easy — without preventing teams from choosing alternatives when needed.Product mindset → treat the platform as a product. Define users (developers), gather feedback, measure adoption, and iterate based on value delivered.Reduce cognitive load → abstract away infrastructure and operational complexity that does not directly contribute to the developer’s core task, i.e., building and shipping business logic. It's imperative to note that Platform Engineering is not DevOps, but DevOps scaled through product thinking — treating developers as customers and the platform as the product. Adoption Journey Large organizations often face delivery challenges that rarely make it into executive summaries. Issues like developer friction, inconsistent and/or duplicate tooling, and fragmented workflows are deeply embedded in day‑to‑day operations. Their impact — delayed releases, inefficiencies, and frustration — may be visible, but the root causes often remain hidden or disconnected from leadership narratives. Thus, the first step is discovery and validation. Organizations must surface real pain points through design thinking workshops, targeted surveys, analysis of past initiatives, and continuous community/user feedback. These insights form the foundation for defining a clear and grounded Platform Mission Statement — one that aligns platform capabilities with genuine organizational needs. Once the mission is clear, enterprises move toward unified platforms that standardize common tools and processes. This consolidation reduces duplication and improves reliability. Guiding Principles should be maintained via ADRs as a standardized platform for uniformity. Also, it's recommended to have decentralized decision-making to avoid bottlenecks and maintain long-term sustainability. To achieve this, use a community-driven approach via various guilds. As maturity grows, self‑service enablement becomes the focus — developers can provision infrastructure, build & deploy applications, perform verification, and integrate monitoring with minimal friction. This can be achieved via Internal Developer Platforms (IDP) and Internal Developer Portals. Finally, mature organizations embrace continuous improvement. The platform evolves like a product — guided by developer feedback and metrics. The feedback loops should act to adapt the platform and be evolutionary in nature. Being preventive or proactive, rather than reactive, goes a long way in achieving a successful platform implementation. Internal Developer Platforms (IDP) vs. Internal Developer Portals A common source of confusion in platform engineering is the distinction between an internal developer platform (IDP) and an internal developer portal. While these concepts are related and often work in tandem, they serve distinct purposes and have different architectural and user experience implications. Internal Developer Platform (IDP) An IDP is the “engine room” of platform engineering. It is a cohesive set of tools, frameworks, and automation scripts that standardize and automate the provisioning, deployment, and management of infrastructure and services. Key components typically include: Self-service infrastructure provisioning → Developers can request and manage resources (VMs, databases, clusters) via APIs or CLI tools, eliminating ticket-based workflows.Unified deployment and orchestration → Standardized CI/CD pipelines, container orchestration (e.g., Kubernetes), and Infrastructure as Code ensure consistent, reliable releases.Centralized configuration and secrets management → Version-controlled settings, automated secret management, and policy enforcement across environments.Automated monitoring and observability → Integrated metrics, logs, and tracing provide real-time visibility into system health.Security and compliance automation → Policy-as-code frameworks (e.g., OPA) enforce security and compliance at every stage. IDPs are typically built and maintained by platform engineering teams and are consumed by developers and operations teams to accelerate software delivery and reduce operational risk. Internal Developer Portal An internal developer portal is the “front door” to the platform. It provides a user-friendly interface (often a web dashboard) that aggregates documentation, service catalogs, APIs, and organizational guidelines. Key features include: Service catalog → Centralized inventory of services, APIs, and infrastructure, supporting discoverability and ownership tracking.Integration ecosystem → Unified view of the development toolchain, integrating with version control, CI/CD, observability, and project management tools.Self-service workflows → Guided forms and wizards for routine operations (e.g., provisioning, deployments), with built-in approval workflows and RBAC.Onboarding and knowledge sharing → Centralized documentation, onboarding guides, and community Q&A features to accelerate ramp-up and collaboration.Metrics and scorecards → Dashboards tracking service health, maturity, and compliance, providing actionable insights for improvement. Portals are typically used by application developers, product teams, and managers to discover services, access documentation, and initiate self-service workflows. When to Use Each (or Both) Start with an IDP when the primary pain points are manual infrastructure provisioning, inconsistent environments, or the need for standardized automation.Start with a Portal when discoverability, onboarding, and knowledge sharing are the main challenges, or when existing tools are underutilized due to lack of visibility.Combine Both for maximum impact: the IDP provides the backend automation and guardrails, while the portal exposes these capabilities through an intuitive, developer-friendly interface Team Topologies and Platform Engineering The success of platform engineering is deeply influenced by organizational structure and team interactions. The “Team Topologies” framework provides a powerful lens for designing team structures that optimize for effective platform adoption Four Fundamental Team Types Stream-Aligned Teams → Aligned to a flow of work from a business domain (e.g., a product or service). They own the end-to-end delivery and operation of features.Platform Teams → Build and maintain internal platforms that provide reusable services and capabilities to stream-aligned teams, reducing their cognitive load.Enabling Teams → Help stream-aligned teams overcome obstacles, adopt new technologies, or fill skill gaps.Complicated Subsystem Teams → Own subsystems that require deep specialist knowledge (e.g., advanced algorithms, core infrastructure). Three Team Interaction Modes Collaboration → Teams work together for a defined period to discover new solutions.X-as-a-Service → One team provides a service that another team consumes with minimal interaction.Facilitation → One team helps another team acquire new skills or capabilities. Relevance to Platform Engineering Platform Teams as Product Teams → Platform engineering teams should operate as product teams, treating stream-aligned teams as customers, gathering feedback, and iterating on platform featuresReducing Cognitive Load → The primary goal of the platform team is to reduce the cognitive load on stream-aligned teams, enabling them to focus on delivering business value rather than infrastructure concernsClear Interfaces and Boundaries → Well-defined APIs, documentation, and support channels ensure that platform capabilities are discoverable and consumable as a service, minimizing dependencies and handoffsContinuous Adaptation → Team boundaries and responsibilities should evolve as business needs and technologies change, with feedback loops guiding organizational adjustments Conway’s Law and Its Applicability to Platform Engineering Conway’s Law, articulated by Melvin Conway in 1967, states that “organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations” In the context of platform engineering, this law has profound implications. How Conway’s Law Shapes Platform Design Organizational Silos Lead to Siloed Platforms → If engineering teams are organized in silos (e.g., by business unit or product line), the platforms they build will reflect this fragmentation, resulting in duplicated tools, inconsistent practices, and integration challengesCross-Functional Collaboration Enables Cohesive Platforms → Successful platform engineering requires cross-functional teams that span development, operations, security, and compliance, ensuring that the platform addresses the needs of all stakeholders and avoids becoming a new silo.Intentional Organizational Design → To achieve coherent, scalable platforms, organizations must deliberately design their communication structures and team interactions to support shared standards, rapid feedback, and continuous improvement. Real-World Example - Monolith to Microservices → Organizations transitioning from monolithic architectures to microservices often reorganize teams around domains or services. If team boundaries are not aligned with desired system boundaries, the resulting architecture may become fragmented or inconsistent, reflecting the underlying communication patterns rather than optimal technical design Platform Engineering as Both Solution and Symptom → Platform engineering often arises as a response to the silos and fragmentation created by previous organizational structures. However, if not implemented with a product mindset and cross-team alignment, platform engineering can inadvertently create new silos, perpetuating the very problems it seeks to solve Metrics and KPIs to Measure Platform Success Measuring the impact of platform engineering is essential for demonstrating value, securing buy-in, wider adoption, and guiding continuous improvement. Common Metrics DORA Metrics → Deployment frequency, lead time for changes, change failure rate, mean time to recovery (MTTR).SPACE Framework → Satisfaction and well-being, performance, activity, communication and collaboration, efficiency and flow.Adoption Rates → Percentage of teams and services using the platform.Time to Onboard → Time required for new developers to become productive.Operational Metrics → Incident rates, uptime, resource utilization, and cost savings.Developer Satisfaction: Surveys, Net Promoter Score (NPS), and qualitative feedback. Feedback Mechanisms Surveys and Office Hours → Regular check-ins with platform users to gather qualitative and quantitative feedback.Telemetry and Usage Analytics → Automated tracking of platform usage, feature adoption, and workflow bottlenecks. Limitations and Common Challenges of Platform Engineering Despite its benefits, platform engineering is not without challenges and limitations. Organizational Challenges Resistance to Change → Teams may be reluctant to adopt new tools or workflows, especially if they perceive a loss of autonomy or increased complexity.Alignment and Buy-In → Achieving consensus on standards, priorities, and platform direction can be difficult in large or distributed organizations.Skill Gaps → Building and maintaining platforms requires expertise in infrastructure automation, CI/CD, security, and developer experience, which may be lacking in existing teams. Technical Challenges Overengineering → Building overly complex or rigid platforms can lead to low adoption and maintenance burden.Integration Complexity → Aggregating data and workflows from diverse tools and systems requires careful planning and robust integrations.Legacy Systems and Technical Debt → Integrating with or migrating from legacy tools and architectures can be time-consuming and costly. Cultural Challenges Product Mindset → Treating the platform as a product, with continuous feedback and iteration, is essential but often overlooked.Avoiding the “Golden Cage” → Mandating platform adoption without addressing real developer needs can lead to resentment and shadow. Measurement and ROI Lack of Metrics → Many organizations fail to measure platform adoption, impact, or ROI, making it difficult to justify continued investment or guide improvements. Products Over Projects In platform engineering, the distinction between a project mindset and a product mindset is crucial. Project Mindset Focus → Deliverables, deadlines, and completion.Approach → Work is structured around a defined scope with a start and end date.Outcome → Once the project is “done” the team moves on, often with limited ongoing ownership.Risk → Platforms built this way may stagnate, as continuous improvement and user feedback loops are not prioritized. Product Mindset Focus → Long-term value, user experience, and continuous evolution.Approach → The platform is treated as a living product with ongoing investment, iteration, and support.Outcome → Teams own the platform end-to-end, ensuring it adapts to changing business and developer needs.Benefit → Encourages innovation and alignment with evolving enterprise strategy. In short, with a project mindset, tasks are done as “Deliver and finish,” while with a product mindset, it's “Deliver, own, and evolve.” Relationship Between Platform Engineering, DevOps, and SRE While platform engineering, DevOps, and site reliability engineering (SRE) share common goals, they address different layers of the software delivery lifecycle. DevOps Focus → Cultural and organizational transformation to break down silos between development and operations, emphasizing collaboration, automation, and continuous delivery.Practices → CI/CD, infrastructure as code, shared responsibility for quality and reliability. SRE Focus → Applying software engineering principles to operations, with a strong emphasis on reliability, scalability, and incident response.Practices → Service-level objectives (SLOs), error budgets, automated monitoring, and incident management. Platform Engineering Focus → Building and maintaining internal platforms that provide standardized, automated, and self-service capabilities for development teams.Practices → Product mindset, self-service, golden paths, policy-as-code, and platform-as-a-product. How They Interact Platform engineering provides the foundation on which DevOps and SRE practices can scale, standardizing workflows, embedding automation and compliance, and enabling self-service for developers and operations teams.DevOps and SRE teams collaborate with platform engineers to ensure that the platform supports reliability, scalability, and continuous improvement. Case Studies Netflix used their platform to solve developers’ challenges to manage multiple services and software, knowing which tools exist, and switching contexts between tools. Zalando leveraged their platform to unify the developer experience, promote compliance by default, and improve how the company operated over time. Carlsberg’s Gaia platform automated infrastructure provisioning, embedded compliance, and provided self-service capabilities, reducing manual work and accelerating project delivery. The platform’s success was attributed to cross-functional collaboration, a product mindset, and continuous feedback from developers eBay’s Velocity initiative (2020) boosted engineering productivity, cutting deployment times from 10 days to 1–2 and enabling same‑day mobile releases. Despite technical success, cultural resistance, outdated tech choices, and poor strategic execution prevented business growth. Conclusion Platform engineering represents a paradigm shift in how modern organizations build, deliver, and operate software at scale. By abstracting complexity, standardizing workflows, and empowering developers through self-service and automation, platform engineering accelerates delivery, improves reliability, and optimizes costs. However, its success depends on more than just technology — it requires intentional organizational design, a product mindset, continuous feedback, and a careful balance between standardization and flexibility. As the discipline matures, organizations that invest in platform engineering as a strategic capability will be best positioned to thrive in an increasingly complex and competitive digital landscape. The golden rule of platform engineering → Treat your platform as a product, your developers as customers, and adoption as a metric — not a mandate. Platform 2.0 — The AI‑Native Evolution Platform 2.0 represents the next stage of platform engineering, where artificial intelligence becomes a built‑in capability rather than an add‑on. It transforms platforms from static automation frameworks into adaptive, learning ecosystems that continuously optimize themselves. Core Principles Intelligence at every SDLC stage → AI augments design, development, testing, deployment, and operations with predictive and generative capabilities.Continuous learning → Feedback loops from telemetry and user behavior refine architecture and performance automatically.Autonomous optimization → Platforms self‑tune resources, detect anomalies, and evolve configurations without manual intervention.Human‑AI collaboration → Engineers focus on strategic design and governance while AI handles repetitive and analytical tasks. Platform 2.0 enables faster delivery, higher reliability, and smarter scalability, redefining platform engineering as an AI‑native discipline — intelligent, adaptive, and perpetually evolving. References and Further Reads Platform Engineering — WikipediaWhat is Platform EngineeringInfoQ trend report Platform Engineering as early adoptersThoughtWorks tech radar recommends adopting Platform Engineering as early as Apr/Oct 2021.Team TopologiesDZone survey where nearly half of respondents indicate using platform engineering.
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.
AWS Glue makes it easy to get a PySpark pipeline running quickly. It is significantly harder to build one that stays maintainable as logic grows, performs reliably at scale, and does not quietly accumulate operational debt over time. Most Glue pipelines start simple and become difficult to manage gradually — formulas get hardcoded, modules grow without boundaries, output files proliferate, and before long a single job is doing too many things in ways that are hard to test, hard to debug, and expensive to change. This article presents a set of design principles drawn from production Glue ETL pipelines processing billions of rows. Each principle is independent — you do not need to adopt all of them to benefit from any one. But together they form a coherent approach to building Glue pipelines that are modular, observable, cost-efficient, and built to last. Principle 1: Externalize Logic Into Config, Not Code The single most impactful structural decision in a Glue pipeline is where business logic lives. When formulas, dataset references, column selections, and filter conditions are hardcoded in PySpark, every change requires modifying job code, redeploying, and re-validating the full pipeline. A one-line formula change carries the same deployment risk as a structural refactor. Over time, this creates a strong disincentive to make changes, and the pipeline calcifies. The better pattern is to treat the Spark job as a generic executor and externalize all business-specific declarations into configuration. Formulas are declared as config entries with operands, rounding rules, and output names. Dataset loading behavior — which table, which columns, which filters, whether to cache — is declared per source rather than scripted per job. Schema shapes for complex types are declared explicitly rather than inlined. JSON { "source_table": "headcount_actuals", "database": "finance_db", "select_columns": ["site", "badge_type", "headcount", "fiscal_week"], "filters": [{"column": "is_active", "value": "Y"}], "rename": {"hc_count": "headcount"}, "cache": true } When a new dataset is needed, a new config entry is added — no Spark code changes. When a formula changes, the config entry is updated — no job redeployment required. The job itself becomes stable and generic; only config changes as business requirements evolve. This principle pays increasing dividends over time. Pipelines with externalized logic are faster to modify, safer to deploy, and easier to hand off because the business rules are readable independently of the execution engine. Principle 2: Design Modules With Explicit Boundaries A Glue job that does everything in one place is easy to write and hard to maintain. As pipelines grow, the instinct to add more logic to an existing job accelerates technical debt faster than almost any other decision. The more durable pattern is to decompose computation into modules with explicit input and output contracts. Each module receives one or more DataFrames, applies a focused set of transformations, and produces a named output DataFrame. Modules communicate exclusively through in-memory DataFrame references — there is no disk I/O between stages, no shared mutable state, and no implicit dependency on execution order beyond what the data flow itself requires. Utilities follow the same boundary principle, organized into two layers. Generic pipeline utilities handle cross-cutting concerns — file writing, dataset loading, filtering, deduplication, pivot operations — and are shared across all modules. Module-specific utilities implement transformation logic scoped to a single module and are never invoked outside it. This structure means adding a new module requires only writing its scoped utilities and wiring it into the pipeline. The generic layer is never touched. Existing modules are never at risk from new module development. The downstream benefit is testability. Each module with clean boundaries can be validated independently using mocked PySpark DataFrames with no Glue environment required. Engineers can run pytest locally against individual modules, iterate quickly, and deploy only after local validation passes. Principle 3: Choose Your Job Topology Deliberately A common default in complex pipelines is to split computation across multiple Glue jobs, using S3 as the handoff layer between stages. This is sometimes the right choice — but it should be a deliberate decision, not an instinct. Multi-job topologies make sense when stages have genuinely different compute profiles, when intermediate outputs need to be reused independently by other consumers, or when a stage failure should not force a full recompute from the beginning. In these cases, job separation gives you independent retry boundaries, independent DPU sizing, and the ability to schedule stages on different cadences. Single-job topologies — where the full pipeline runs within one Spark session — make sense when all computation is tightly coupled, modules share the same input datasets, and intermediate outputs have no standalone value. Running everything in one session eliminates cold start overhead for intermediate stages, avoids the cost of serializing data to S3 and deserializing it back between jobs, and keeps the execution model simple to reason about: one trigger, one job, one result. The question to ask is whether the stages truly need to be independent. If intermediate S3 persistence adds coordination complexity without adding value — no independent consumers, no differential retry requirements, no meaningful DPU difference between stages — then collapsing to a single job is usually faster, simpler, and cheaper. If stages have real independence requirements, splitting them is the right call and the operational overhead is justified. Neither topology is inherently superior. The mistake is defaulting to one without evaluating the trade-offs for the specific pipeline at hand. Principle 4: Overlap Writes With Computation When Latency Matters Overlapping writes with computation is a well-established technique in high-performance computing, deep learning training, and heavy database operations. The core idea is to hide the slow latency of I/O operations by running them in the background while the CPU or GPU continues processing data. Rather than waiting for a write to complete before starting the next computation, both proceed simultaneously — I/O latency is absorbed into computation time rather than added on top of it. In Glue ETL pipelines, the same principle applies directly. In a pipeline where multiple output DataFrames are produced, the naive write strategy — complete all computation, then write all outputs sequentially — has two compounding problems. First, it creates a peak memory spike: all computed results are held in memory simultaneously while writes proceed one by one. Second, it serializes work that does not need to be serial: every millisecond spent waiting for S3 acknowledgment is a millisecond the Spark executors are idle. This is worth addressing only when latency is a meaningful constraint. For low-frequency batch jobs running overnight with no user-facing SLA, sequential writes are perfectly adequate. But for pipelines where users or downstream systems are waiting on results — or where job duration directly affects infrastructure cost — overlapping writes with computation delivers measurable wall-clock reduction. The two-phase write strategy implements this directly. Outputs from early modules are written to S3 in background threads immediately after those modules complete, running in parallel with later computation stages. By the time all computation finishes, a significant portion of the output data has already landed in S3. Remaining outputs are then flushed concurrently in a second phase. The implementation leans on Python's concurrent.futures.ThreadPoolExecutor to manage background write threads while the main Spark session continues computation on the driver. A generic write orchestration utility can wrap this pattern so individual modules never need to manage thread lifecycle directly — they simply declare their output and the utility handles scheduling, thread management, and error propagation. Python from concurrent.futures import ThreadPoolExecutor, as_completed def write_phase_a(write_tasks): with ThreadPoolExecutor(max_workers=len(write_tasks)) as executor: futures = {executor.submit(task["fn"], task["df"], task["path"]): task["name"] for task in write_tasks} for future in as_completed(futures): name = futures[future] future.result() logger.info(f"[Phase A] Write complete: {name}") The practical effect is that peak memory pressure is distributed over the job's lifetime rather than concentrated at the end, and total wall-clock time is reduced by the overlap between I/O and CPU-bound computation. For pipelines with many output datasets and a latency SLA to meet, the savings compound significantly. Principle 5: Right-Size Output Files With a Reusable Writer Utility Right-sizing output files is the practice of tuning file sizes to balance disk I/O performance, network transfer speeds, and downstream processing efficiency. Too many small files and downstream readers spend more time on metadata operations and S3 API calls than on actual data reads. Too few large files and parallelism suffers — readers cannot split work efficiently across threads or nodes. The target is consolidated, evenly sized files that match the read patterns of downstream consumers. Spark's default output behavior writes one file per partition, and partition counts are typically tuned for computation throughput rather than output shape. A job optimized for shuffle performance might produce hundreds of partitions, each containing a few megabytes of output data — perfectly reasonable for Spark internals, but harmful for any reader that comes after. This small file problem compounds over time as output partitions accumulate in S3 and the Glue Catalog metadata grows with them. The fix is a reusable writer utility that decouples output file sizing from Spark's internal partition count. Rather than accepting the default, the utility estimates the DataFrame's actual size, calculates the appropriate number of output files for a target file size — typically 128MB to 256MB per file — and coalesces partitions before writing. Python def write_optimized(df, output_path, partition_cols, target_file_size_mb=128): estimated_size_mb = df.rdd.map(lambda row: len(str(row))).sum() / (1024 * 1024) optimal_partitions = max(1, int(estimated_size_mb / target_file_size_mb)) df.coalesce(optimal_partitions) \ .write \ .partitionBy(*partition_cols) \ .parquet(output_path, mode="overwrite") Making this a shared generic utility rather than inline logic in each module has two practical benefits. First, it enforces consistent file sizing behavior across all outputs in the pipeline — no module accidentally writes thousands of tiny files because an engineer forgot to coalesce. Second, it centralizes the tuning knob: when the target file size needs to change — because downstream query patterns shift or a new consumer has different read characteristics — it changes in one place and applies everywhere. Right-sized output files improve Athena scan performance, reduce per-query S3 API costs, keep Glue Catalog partition metadata manageable, and make the output data easier to consume for any downstream system reading from S3. This is a low-effort, high-payoff improvement that applies to virtually every Glue pipeline writing to S3. Principle 6: Use Complex Types to Defer Denormalization SQL-based pipelines are constrained to flat, fully denormalized row structures at every intermediate stage because SQL has no native complex type support. This forces denormalization to happen early, inflating data volume at every subsequent join and aggregation. PySpark has native support for structs, maps, and arrays. Using these types at intermediate stages allows related values to be grouped logically without inflating row counts. A row that would require five denormalized rows in SQL can be represented as a single row with a struct or array column in Spark. Denormalization is then deferred to the final output layer only — applied once, at write time, for consumers that require flat structures. Everything upstream of the final write benefits from reduced volume, fewer shuffles, and faster joins. This principle is particularly impactful in pipelines with multi-level aggregations or wide schemas where dozens of metrics attach to the same dimensional key. Keeping those metrics grouped in a struct until the final output stage reduces the effective row count and join complexity throughout the pipeline. Principle 7: Build Observability Into Every Stage Glue jobs that fail silently or surface errors as opaque stack traces at the end of a long execution are expensive to debug. The investment in step-level observability pays back quickly the first time something goes wrong in production. The minimum viable observability pattern is row count logging at every materialization point. After each module completes and after each write, log the output row count with a descriptive label. This gives a running picture of data volume through the pipeline and makes it immediately obvious when a transformation has dropped rows unexpectedly or produced more rows than expected. Python def log_step(df, step_name): count = df.count() logger.info(f"[{step_name}] Row count: {count:,}") return df Pair this with a try/except/finally pattern at the job level that ensures spark.catalog.clearCache() is always called on exit — whether the job succeeds or fails — to release cached DataFrames and avoid memory leaks across retries. Python try: run_pipeline() except Exception as e: logger.error(f"Pipeline failed: {e}") raise finally: spark.catalog.clearCache() CloudWatch captures all logs automatically. When a job fails, the row count trail shows exactly where in the pipeline the problem occurred, making triage faster and reducing the time between failure and fix. Principle 8: Isolate Executions for Concurrency Pipelines that share compute resources across simultaneous executions create contention that is difficult to predict and expensive to manage. The common response — queue-based serialization — adds operational complexity without solving the underlying resource constraint. AWS Glue's execution model eliminates this problem structurally. Each job execution gets its own isolated DPU allocation. There is no shared compute pool. Ten simultaneous executions consume ten independent DPU allocations and do not interfere with each other in any way. Designing for this means treating each execution as fully independent: no shared state, no cross-execution coordination, no assumption about what other executions are running. Combined with idempotent writes — using overwrite mode so a retry produces the same result as the original execution — the pipeline becomes safe to run concurrently at any scale without additional coordination logic. The cost model reinforces this. Glue bills per DPU-second of actual compute consumed. An execution that takes eight minutes on 240 DPUs costs the same whether it runs alone or alongside a hundred other executions. There is no premium for concurrency and no shared pool to provision for peak load. Putting It Together These eight principles are independent but complementary. A pipeline that applies all of them is modular enough to develop in parallel, observable enough to debug quickly, cost-efficient enough to run at scale, and stable enough to maintain over time without accumulating structural debt. The quickest wins for most existing pipelines are Principles 1, 5, and 7 — externalizing logic into config, right-sizing output files with a shared utility, and adding row count logging at every stage. Each can be applied incrementally without restructuring the full pipeline. The remaining principles become more valuable as pipeline complexity grows and concurrency requirements increase. The underlying thesis is simple: a well-designed Glue pipeline should be easy to change, easy to test, easy to debug, and cheap to run. None of those properties require exotic infrastructure. They require deliberate design decisions applied consistently from the start.
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.
AI Doesn't Replace Your Architecture; It Becomes Part of It Picture this. Your team has just integrated a large language model into your enterprise application. The demo looked compelling. The agent interpreted user intent, called several APIs, and returned a coherent result. Everyone in the room was impressed. Then the questions started. What happens when the LLM misinterprets a request and calls the wrong API? Who owns the business logic embedded in that prompt? If the model changes, does the integration break? How do you audit what the AI decided and why? These aren't AI questions. They're architecture questions, and they don't go away just because you've added intelligence to the system. The most important architectural decision you'll make about AI isn't which model to use. It's where the AI sits relative to your existing integration layers. Get that right, and AI becomes a powerful, governable component in a coherent system. Get it wrong, and you'll end up with business logic scattered across prompts, brittle integrations that break when the model updates, and no clear line of accountability when something fails. The question isn't "Can AI call APIs?" It's "Where should AI sit within your architecture?" There are three architectural roles worth separating clearly. API facade. The edge layer that translates external requests into internal operations.Workflow orchestration. The layer that manages multi-step business processes and decision logic.Event-driven integration. The layer that lets systems react to changes without tight coupling. Each serves a different purpose, and AI belongs in different places depending on the business problem you're solving. Figure 1 lays out all three roles side by side, including what AI owns and does not own in each one. Figure 1. Where AI Sits: Three Architectural Roles The table below gives a quick reference for how the three patterns differ before we walk through each one in detail. Pattern Purpose Coupling Determinism Where AI Fits API Facade Translate external requests into internal operations Tight, synchronous Low, request-driven Interpreting intent, extracting parameters Workflow Orchestration Sequence multi-step business processes Moderate, coordinated High, explicit branching Providing probabilistic input to decision points Event-Driven Integration Let systems react to change asynchronously Loose, decoupled Variable, per consumer Consuming and enriching events, never the bus itself This article walks through where AI fits within each pattern, and just as importantly, where it doesn't. 1. Start by Defining What the AI Is Responsible For Before you touch an integration pattern, answer a more fundamental question. What is the AI actually accountable for in this system? This sounds obvious but gets skipped constantly. Teams reach for an LLM because it handles natural language well, then gradually load it with responsibilities it shouldn't own, like validating business rules, managing state, enforcing authorization logic, and driving deterministic workflows. The AI ends up doing everything, which means the architecture owns nothing clearly. Ask these questions before making any integration decisions. Is the AI interpreting human input? Natural language understanding, intent classification, and entity extraction are AI-native tasks where models genuinely add value.Is the AI making recommendations or decisions? A recommendation, such as "this customer is likely to churn," is a probabilistic output. A decision, such as "cancel this subscription," is a deterministic action with business consequences. These require different ownership models.Is the AI coordinating business processes? If yes, be careful. Orchestration logic embedded in prompts is invisible to your governance tooling, untestable in any traditional sense, and will silently drift as the model updates.Which steps require human approval? Any action that is irreversible, regulated, or high stakes should have an explicit human checkpoint that lives in your workflow layer, not inside a prompt. The cleaner your answer to these questions, the cleaner your integration design will be. Blurry responsibilities produce brittle architectures. Define the boundary first. 2. AI at the API Facade, the Conversational Edge The API facade pattern sits at the edge of your system. It's the layer that translates external requests into internal operations. Traditionally, this meant REST or GraphQL endpoints that routed structured requests to back-end services. AI belongs here when the primary challenge is bridging the gap between unstructured human intent and structured system operations. Think of an enterprise procurement assistant. A buyer types, "Reorder the same supplies we used for the Sydney office fit-out, but increase quantity by 20% and flag anything over $5,000 for manager approval." No traditional API handles that sentence on its own. The facade layer is exactly where an LLM adds value. It parses intent, extracts parameters, resolves ambiguity, and maps the request to specific downstream API calls. What AI does well at the facade includes intent resolution, turning natural language into structured API parameters. It also handles entity extraction, pulling order IDs, product codes, dates, and names from conversational input. It supports contextual disambiguation, using conversation history to resolve references like "that vendor" back to a specific vendor ID mentioned earlier. And it enables response synthesis, taking structured API responses and returning natural language answers. What AI should not own at the facade is just as important. Authorization logic belongs in your API gateway or identity layer. Rate limiting and throttling are infrastructure concerns, not model concerns. Core business rules, such as "orders over $5,000 require approval," should live in your workflow layer rather than in a prompt where they're invisible to compliance tooling. The practical pattern is that AI at the facade acts as a structured parameter extractor. It takes conversational input, produces a clean structured intent object, and hands off to APIs that were designed for deterministic consumption. The model interprets. The API executes. The example below shows what that structured intent object might look like once the model has parsed the procurement request above. JSON { "intent": "create_purchase_order", "reference_order": "sydney_office_fitout_2026", "quantity_multiplier": 1.2, "approval_required_above": 5000, "currency": "USD", "extracted_from": "conversational_input", "confidence": 0.94 } Listing 1: Example structured intent object produced at the API facade. Design your facade APIs to accept both human-readable context and machine-structured parameters. Build explicit validation at the API boundary so that when the model produces a malformed or out-of-range parameter, the error is caught and surfaced clearly, not silently swallowed or, worse, acted upon incorrectly. 3. AI Inside Orchestration, Where Flexibility Meets Business Workflows Workflow orchestration manages multi-step business processes, including the sequence of steps, branching logic, error handling, retries, and human approval gates. It's the layer that knows how work gets done, in what order, and under what conditions. The central tension when introducing AI into orchestration is that orchestration is deterministic by design, while AI is probabilistic by nature. A well-governed workflow produces the same output given the same inputs. An LLM does not. Mixing these carelessly produces workflows that are auditable on paper but unpredictable in practice. The architectural resolution is to keep the orchestration layer deterministic while allowing AI to provide probabilistic inputs into specific decision points. Think of AI as a specialized step inside the workflow, one that produces an output that the workflow then acts on according to explicit, auditable logic. A claims processing workflow illustrates this well. The overall process — intake, validation, AI-assisted assessment, human review, approval, and payment — is orchestrated deterministically. The AI participates at the assessment step. It analyzes claim documentation and produces a structured output: an estimated validity score, a list of missing documents, and a recommended action. The workflow then applies explicit branching logic. A score above 0.85 triggers auto approval. A score below 0.4 gets flagged for denial review. Everything in between routes to a human adjudicator. The AI informs. The orchestration decides. Figure 2 shows this flow end to end. Figure 2. AI Inside Orchestration: Claims Processing Workflow A few design principles matter here. Treat AI steps as typed operations with defined inputs and outputs. The orchestration layer should pass a structured payload to the AI and receive a structured response, not an open-ended conversation. This makes the AI step testable, replaceable, and governable. The snippet below shows a minimal example of what a typed contract for an AI step might look like. TypeScript // Typed contract for an AI step inside orchestration interface ClaimAssessmentInput { claimId: string; documents: DocumentRef[]; } interface ClaimAssessmentOutput { validityScore: number; // 0.0 to 1.0 missingDocuments: string[]; recommendedAction: "approve" | "review" | "deny"; } Listing 2: Example typed input/output contract for an AI step inside an orchestrated workflow. Never let the AI own branching logic that has compliance or audit implications. If a decision must be explainable to a regulator, it should live in the orchestration layer where it's visible, versionable, and logged. Design explicit human approval gates. In enterprise workflows, AI recommendations that trigger consequential actions, such as financial transactions, customer notifications, or system changes, should route through a human checkpoint unless you've explicitly validated and signed off on full automation. Build retry and fallback paths. An AI step that fails, times out, or returns a low-confidence result needs a defined fallback, whether that's routing to a human, using a default, or escalating, built into the orchestration rather than handled ad hoc in the calling code. Platforms like OutSystems, which provide visual workflow design alongside AI integration capabilities, make this separation of concerns tangible. You can see exactly where in the process flow an AI step participates, what it receives, and what happens next based on its output. 4. AI and Event-Driven Architecture, Reacting Without Controlling Event-driven architecture decouples systems through a shared event bus. Producers emit events when something happens, and consumers subscribe and react without either party knowing the other exists. It's the pattern that makes large distributed systems composable and independently evolvable. AI fits naturally into event-driven systems, but as a consumer and enricher, not as the event bus itself. The pattern works like this. A transactional system emits a clean, well-defined business event, such as OrderPlaced, CustomerChurnRiskFlagged, or SupportTicketOpened. An AI consumer subscribes, processes the event asynchronously, and either emits a derived event, like ChurnRiskClassified or TicketCategorized, or writes to a downstream store. Core transaction systems remain untouched. This architecture has a key property for AI integration, which is isolation. The AI component can be updated, replaced, or retrained without touching the transactional system that produced the event. The event schema is the contract between them. As long as the AI consumer honors its output schema, the downstream systems don't care what model is running behind it. AI adds value in event-driven systems in several ways. Real-time classification lets an incoming support ticket event trigger AI categorization and routing before a human ever sees it. Anomaly detection allows a stream of transaction events to feed an AI consumer that flags unusual patterns and emits a FraudSignalDetected event. Content enrichment means a DocumentUploaded event can trigger an AI pipeline that extracts entities, generates a summary, and writes structured metadata back to the event stream. A few cautions are worth noting too. Don't use AI to produce events that trigger irreversible transactional operations without a validation step. An AI-emitted event that directly drives a financial settlement or account closure is a governance risk. Keep AI consumers idempotent, since event-driven systems often deliver events at least once, and your AI consumer should produce the same output for the same event input regardless of how many times it processes it. Version your event schemas independently of your AI models. When the model changes, the event contract should remain stable. Break this rule, and you'll find yourself coordinating model updates with schema migrations across multiple teams. 5. Design APIs for AI Variability, Not Just Traditional Applications Traditional API design assumes well-behaved clients. They send valid, structured requests, handle errors predictably, and operate within known parameters. AI agents are different clients. They may generate requests outside expected parameter ranges, retry with slight variations when uncertain, pass natural language fragments where IDs are expected, or call endpoints in unconventional sequences. This changes how APIs should be designed when AI is a first-class consumer. Be explicit about parameter constraints and semantics. Document not just the type of a parameter but what it means and what values are valid. An AI agent that doesn't understand that "customer_status" is an enum with five specific values will guess, and it may guess wrong. Explicit schemas with enumerated values and clear descriptions dramatically reduce the error surface. Return structured, self-describing error responses. When an AI agent calls an API and gets a validation error, the response should tell the agent exactly what was wrong and what correction is expected. A generic 400 with "invalid input" gives the agent nothing to act on. A structured error that says the field "quantity" must be a positive integer, and that a negative value was received, allows the agent to self-correct on retry. Design for idempotency on write operations. AI agents may retry failed calls. Any write operation that could be called multiple times should be idempotent, meaning calling it twice with the same payload should produce the same result as calling it once. This is a baseline requirement for reliable agentic workflows. Consider AI-specific API profiles alongside your standard endpoints. Some teams are building enriched API descriptions, effectively structured, semantic documentation that LLMs can consume during function calling or tool use scenarios. These profiles describe not just syntax but intent, preconditions, and expected postconditions. If your platform supports it, these descriptions significantly improve agentic reliability. 6. Preserve Loose Coupling as AI Capabilities Evolve If there is one thing that is certain about the current AI landscape, it's that it will look different in 18 months. Model capabilities are improving rapidly. New reasoning architectures, longer context windows, better function calling, and multimodal inputs will change what AI can reliably do, which means the design decisions you make today about where AI participates in your architecture will need to evolve. The integration architectures that will age best are the ones that treat AI as a replaceable component behind a stable interface, not as a load-bearing structural element that the rest of the system is built around. Practically, this means a few things. The interface between your AI component and the rest of the system should be typed and versioned, just like any other service boundary. If you replace the LLM behind that interface with a better model, the orchestration layer and downstream consumers shouldn't need to change. Business logic should not live in prompts. Prompts that embed business rules, such as approval thresholds, eligibility criteria, or routing conditions, will drift as models are updated and will be invisible to your governance tooling. Extract that logic into the orchestration or rules layer where it can be versioned and audited. Test AI steps in isolation. Build evaluation harnesses that validate the AI component's outputs against known good test cases. When you upgrade a model, run the evaluation before you promote to production. This is standard software engineering discipline. It just hasn't been applied consistently to AI components yet. Plan for model-level fallback. If a primary model is unavailable or underperforming, your architecture should support routing to a fallback. This is easier to build in advance than to retrofit during an incident. The teams that will maintain architectural coherence as AI evolves are the ones that applied the same separation of concerns discipline to AI components that they've always applied to services, databases, and APIs. 7. Build Observability Across AI and Integration Layers Debugging traditional distributed systems is hard. Debugging systems where one of the components is an LLM is harder. The failure modes are different. The system may be technically healthy while producing incorrect, inconsistent, or subtly wrong outputs. A 200 OK from an AI step tells you the HTTP call succeeded. It says nothing about whether the response was accurate, relevant, or safe. Observability in AI integrated architectures needs to span multiple layers simultaneously. At the AI component level, teams should capture the full prompt sent to the model, not just the output, along with the raw model response before any parsing or post-processing. Token counts, latency, and model version matter too, as do confidence scores or reasoning traces where the model provides them, and retry attempts or fallback triggers. At the integration layer, capture which APIs the AI called, with what parameters, and what the responses were. Track workflow step durations and branching decisions, event payloads at each stage of processing, and human review decisions and overrides. At the business outcome level, ask whether the end-to-end process completed successfully, whether AI-assisted decisions matched expected patterns, and where AI components are producing outputs that require human correction. Platforms that provide centralized monitoring across application logic, integrations, and workflows, such as OutSystems, reduce the instrumentation burden by giving teams a single observability surface rather than requiring separate tooling for each layer. This matters most during incident response, when you need to trace a failure from a user-visible symptom back through the AI component, through the API calls it made, and into the underlying workflow state, quickly. One practice worth establishing early is shadow mode evaluation. Before promoting AI-assisted decisions to full automation, run the AI in parallel with existing logic and compare outcomes without acting on the AI's output. This builds confidence in the model's reliability on your specific data distribution before you depend on it in production. Conclusion. Integration Architecture Is Still the Foundation AI agents are sophisticated components, but they're still components. They have inputs and outputs. They can fail. They need to be tested, monitored, versioned, and replaced, and crucially, they need to sit somewhere coherent in your architecture. The teams that will get the most out of AI are the ones that ask the architectural questions first. What is the AI responsible for? Where does its output go? Who owns the logic around it? How will we know when it's wrong? The answer isn't a different architecture for AI. It's the same architectural discipline that enterprise systems have always required, applied with precision to a new kind of component. API facade, orchestration, and event-driven architecture were built to manage complexity, enforce separation of concerns, and keep systems evolvable. AI makes all three more valuable, not less. The question is simply where, within each, the intelligence belongs. References APISDOR. "How AI Agents Are Reshaping Enterprise Software Architecture." 2026. https://www.apisdor.com/blog/how-ai-agents-are-reshaping-enterprise-software-architecture/Elementum. "Enterprise AI Orchestration: Complete Architecture Guide." 2026. https://www.elementum.ai/blog/enterprise-ai-orchestration-architectureDevRev. "AI Agent Orchestration: Patterns, Pitfalls & the Shared Memory Architecture." 2026. https://devrev.ai/blog/ai-agent-orchestrationViston AI. "Architecture for Enterprise AI Orchestration: A 2026 Blueprint." 2026. https://viston.tech/recommending-a-production-ready-architecture-for-enterprise-ai-orchestration/"Autonomous Event-Driven Multi-Agent Orchestration for Enterprise AI at Scale." arXiv, 2026. https://arxiv.org/pdf/2606.20058Zuplo. "The API Readiness Gap: How to Design APIs That AI Agents Can Actually Use." 2026. https://zuplo.com/learning-center/api-readiness-gap-agent-callable-apis freeCodeCamp. "How to Design APIs for AI Agents." 2026. https://www.freecodecamp.org/news/how-to-design-apis-for-ai-agents/"Self-Reflective APIs: Structure Beats Verbosity for AI Agent Recovery." arXiv, 2026. https://arxiv.org/pdf/2606.05037 "Building Customer Support AI Agents at 100M-User Scale: An Evaluation-Driven Framework." arXiv, 2026. https://arxiv.org/pdf/2606.08867"Characterizing Faults in Agentic AI: A Taxonomy of Types, Symptoms, and Root Causes." arXiv, 2026. https://arxiv.org/pdf/2603.06847 Agentive AI Agents. "AI Agent Error Handling: 7 Proven Practices." 2026. https://agentiveaiagents.com/ai-agent-error-handling-best-practices/
A multi-agent AI system is comprised of several independent but cooperating agents who are working towards a common goal through interaction with one another. In warehouse logistics, different types of agents can be represented as: Inventory control systemsRobotic picking unitsTransportation and/or routing systemsDemand planning engines The real power of multi-agent AI in operating warehouses is in how these agents can collaborate. For instance, if an agent finds an increase in demand, that agent can work immediately with inventory and fulfillment agents to make changes to their operations without having to wait. Key Applications of Multi-Agent AI in Warehouse Logistics The actual benefits of multi-agent artificial intelligence can be revealed by examining how it functions within differing warehouse-related tasks. From managing inventory to fulfilling customer orders, multiple intelligent agents collaborate in order to streamline operations, ultimately increasing the efficiency of the warehouse. These methods showcase how utilizing multi-agent AI in warehouse operations allows companies to automate highly complicated tasks as well as make quicker and data-driven decisions. 1. Intelligent Inventory Management Inventory management is one of the most crucial warehouse-related functions. Poor inventory control can significantly impact a company's profitability due to stock-outs or overstocked items. When using multi-agent AI to manage inventory, agents will continuously monitor stock levels, assess demand patterns, and trigger automatic replenishment of stock. Agents not only predict shortages before they transpire, but they also ensure that stock levels remain at the optimal threshold. 2. Smart Order Fulfillment Order fulfillment requires multiple steps, including picking, packing, and shipping; therefore, it can sometimes be a challenge to coordinate these processes efficiently. Multi-agent systems make this easier by assigning distinct roles to different agents involved in the order fulfillment process. Picking agents identify the quickest route to retrieve product from the warehouse shelves; packing agents ensure that the fulfillment of orders is executed in an efficient manner, and shipping agents coordinate the scheduling of the dispatch of product to customers. The result of multi-agent AI utilized in warehouse operations is improved speed and accuracy of order fulfillment, creating additional value for customer satisfaction levels. 3. Optimizing Routes During Transit To minimize delays, it is important for either a human or a robotic worker to move through a warehouse in an effective manner. By utilizing a multi-agent system to analyze real-time conditions such as congestion levels and workload distribution, the system can generate the most effective route for workers to follow, resulting in faster travel times and improved efficiency of operations. Through intelligent navigation, a warehouse can accommodate an increased volume of products without difficulty. 4. Forecasting Demand and Planning To effectively manage inventory levels and plan for the appropriate staffing of employees, it is critical that accurate forecasts of demand be generated. Multi-agent AI utilizes past trend data, seasonal cycles, and current market forces to deliver accurate predictions of demand. The cooperation of different agents enables the generation of an accurate forecast and allows for real-time adjustment of forecasts and strategies. Therefore, multi-agent AI provides warehouse operators with a powerful resource to expedite decision-making. 5. Maximizing The Workforce Effectively In addition to managing inventory and technology, managing human resources effectively will provide the best results. Multi-agent AI can provide employees with task assignments based on their current skills, establish an optimal work schedule, and oversee employee performance, thus increasing workforce efficiency and decreasing efficiency loss due to operations. Challenges to Consider in multi-agent AI in warehouse operations Even though the benefits of using multi-agent AI in warehousing operations are significant, there are also some potential problems with implementing multi-agent AI. Businesses that implement multi-agent AI should carefully plan for successful implementation and long-term success. The following list describes the key challenges: 1. Complexity of Integration Integrating multi-agent AI with existing warehouse systems can present a technical challenge. Many warehouses use legacy software systems that will likely not support the advanced capabilities of today’s AI. In order for the old and new systems to communicate seamlessly, careful planning and technical expertise are required. 2. Upfront Costs In order to implement an AI-based solution, businesses typically incur significant up-front costs associated with infrastructure, software, and training. Implementing an AI-based solution can be a substantial barrier for small and mid-sized businesses. However, over time, the efficiency improvements usually offset these initial costs. 3. Reliance on Accurate Data Multi-agent systems rely on accurate real-time data to operate efficiently. Poor data quality or incomplete data sets that need to be used may lead to erroneous decision-making by multi-agent systems, thus resulting in decreased performance. To obtain optimal results from multi-agent systems, businesses will need to invest in solid data management practices. 4. Security Threats As various agents frequently interact and exchange information, it is essential to ensure security as a priority because if there is a weakness in your security controls, sensitive operational data will be subject to exposure. Therefore, you must consistently implement comprehensive cybersecurity measures and perform ongoing monitoring to safeguard your overall system's integrity. Best Practices for Implementation An effective multi-agent AI implementation involves some planning to make sure that it is not rushed, which ensures smoother integration, better outcomes, and long-term scalability through the proper strategy for the implementation process. Here are some of the practices that can help your business to implement multi-agent AI successfully. 1. Set clear objectives The initial step of effective implementation is to have a defined understanding of your objectives. What do you wish to accomplish? Improved efficiency, lowered costs, faster order fulfilment? Defining your clear objectives will help you identify which AI to pursue and how to measure success. Without defined goals and objectives, the implementation can easily become unfocused and therefore not result in the desired outcomes. 2. Construct the right data structure Multi-agent systems rely on real-time data to function. As such, the implementation of a strong data structure is essential. The data structure must have the proper data collection, storage, and processing capabilities to ensure that the data remains of high quality and can therefore provide high-quality insights leading to informed decision-making throughout all your warehouse operations moving forward. 3. Start small and grow Don’t attempt to implement AI into an entire warehouse all at once; instead, start with a pilot project. This will allow you to test performance, discover problems, and make adjustments before proceeding to implement AI into the entire warehouse. Implementing in phases reduces the level of risk and ensures a smoother transition of implementation. 4. Selecting the Proper Technological Partner Choosing an experienced AI development company can improve your chances of success when implementing a project. They will help you choose the right tools, design the system, and avoid many of the pitfalls that occur when implementing these systems, which leads to faster and more efficient deployment processes. Conclusion The speed at which the world of warehouse logistics is changing requires businesses to adopt new systems to accommodate the increased demands of their operations, as traditional approaches will no longer suffice. The introduction of multi-agent AI for warehouse operations represents a major breakthrough, allowing for real-time operational coordination, improved decision-making, and increased efficiency in day-to-day activities. Companies can take advantage of this technology by improving their internal procedures and processes to save money and increase accuracy throughout their supply chain. In order to gain maximum benefit from this type of technology, companies will want to contract with AI Development Services and use quality services.
Compliance Reporting Without Losing the Spreadsheet or the Control
July 14, 2026
by
CORE
Scaling Teams, Scaling Systems: Unlocking Developer Productivity With Platform Engineering
July 14, 2026
by
CORE
From Gherkin to Source Code Without Losing the Business Language
July 14, 2026 by
How to Build a Brand Monitoring Dashboard With SerpApi and Python
July 14, 2026 by
12 Factor Framework for Building Secure and Compliant Cloud Applications
July 14, 2026
by
CORE
GraphRAG in Practice Using Spring AI, Neo4j, and Goodreads Data
July 14, 2026
by
CORE
How to Build a Brand Monitoring Dashboard With SerpApi and Python
July 14, 2026 by
12 Factor Framework for Building Secure and Compliant Cloud Applications
July 14, 2026
by
CORE
How to Build a Brand Monitoring Dashboard With SerpApi and Python
July 14, 2026 by
12 Factor Framework for Building Secure and Compliant Cloud Applications
July 14, 2026
by
CORE
GraphRAG in Practice Using Spring AI, Neo4j, and Goodreads Data
July 14, 2026
by
CORE
July 14, 2026
by
CORE
Scaling Teams, Scaling Systems: Unlocking Developer Productivity With Platform Engineering
July 14, 2026
by
CORE
AWS Glue ETL Design Principles for Production PySpark Pipelines
July 14, 2026
by
CORE
GraphRAG in Practice Using Spring AI, Neo4j, and Goodreads Data
July 14, 2026
by
CORE
Compliance Reporting Without Losing the Spreadsheet or the Control
July 14, 2026
by
CORE
Differential Flamegraphs in Java in Jeffrey Microscope
July 14, 2026
by
CORE