In this part 3 of 5 for the series, we’re going to look at the “How”of digital connectivity among products.
A taxonomy of anything is useful until it becomes a giant set of overwhelming boxes. One way to illustrate the differences for this set of things to use examples and show where the parts can be applied in useful ways. This will ideally be more effective than just a basic chart alone.
Let’s pretend we have a logistics service. We’ll call it ScottsHouseOfLogistics. (Because no one is going to bother getting that URL. Probably. And this way I have a place to put all the stuff I want to sell.) Ideally we’ve got customers. So we’ll also have packages, drivers, delivery stops, tracking events and support cases. Our overall realm will stay the same. What changes are the interaction scenarios.
We won’t simply try to implement one function eight times and declare a winner. These patterns aren’t necessarily competitors. In a real product, several may appropriately coexist because they’re solving different interaction problems. Any PM that’s been around awhile has seen choices made based on what “we happen to have on hand and know right now.” That might be a very bad idea, but quite often can actually be the best course of action. You use the tools you have available. But is that always the best practice? Unlikely. This isn’t about you / us learning to call all these shots. That’s generally not our core skill set. And we shouldn’t be “that person” getting into it with your trusted tech counterparts. Still, if it’s your product and you’re going to be on the hook for success / failure, maybe P&L, then you should at least be asking the right questions or understand what your colleagues are telling you about such architectural choices.

Learning By Example
Oftentimes it’s easier to understand something when you can see a working example and manipulate it yourself. So for the examples I’m going to describe below, I’m going to offer two opportunities to do that.
- Google Colab Sample Notebook: Google Colab is a system that uses something called notebooks that let you put explanatory text inline with Python code. You can actually run the code examples just by clicking a little button next to each one. If you want, you can copy the notebook to your own Google Drive and play with things, modify them, etc. For these examples you do not have to be overly technical. You will see a lot of code, but all you have to do to follow the examples is click on a few GO buttons. The instructions are in the Notebook.
Link to Colab Notebook: Open the Google Colab Notebook - Postman: Postman is a product that lets you access, inspect and test a variety of software interfaces. It can directly work with several of the examples below, including REST, GraphQL, gRPC, WebSockets and MCP. It’s less natural for some of the larger system-to-system or agent-workflow examples, so in a later separate “bonus” article, I’m going to show which of these can be explored in Postman and how. These are somewhat more challenging than using the notebook above as you’ll need to navigate the software and enter a bunch of parameters. Also, the interface may change over time so you may have to interpret a thing or two as you go as I might now always keep this article up to date.
Link to Bonus Article on Postman: Coming Soon
Disclaimer: These samples all worked as of the time I built these articles. There is no guarantee that all of the free APIs or open source tools I’m using will remain available or that I’m going to be able to keep up with any such changes over time.
If you just want a quick skim on examples, here they are.
REST Is an Easy Choice for a Known Resource
Let’s say a merchant wants the current status for package 123. It knows exactly what it wants. It is essentially saying, “Give me the current record for this package.”
So it could make a request such as:
GET /packages/123
And get back something like:
{
"id": "123",
"status": "Out for delivery",
"estimatedDelivery": "3:30 PM",
"latestEvent": "Loaded onto delivery vehicle"
}

The server has decided what a standard package response looks like, and that contract is documented in the API. If five different partner systems request package 123, they will generally get that same defined kind of response and each can use the parts it needs.
For a less technical PM, the useful mental model is: “I know the thing I want. Give me that thing in the agreed format.”
The PM question is: Does the consumer know what resource or action it wants, and is a predictable request-and-response contract enough? If yes, REST may be the boring answer in the best possible way.
There’s nothing wrong with good old HTTP APIs here. You don’t get points for fancier technology. You want an effective and efficient solution that partners can actually use. A mature industry may also already have skills, security controls and integration infrastructure built around familiar API patterns. Actually, they probably do. It may even be the case that you have to slow down delivery of some features because a whole ecosystem isn’t ready to adopt them. Or at least, you may have to support legacy systems for some time, even if you have better upgrade options. This might be especially true if some of the folks using these older systems are your very large and longest running customers who have massive investments in existing systems and might not feel the benefit of changing. All things to take into account.
GraphQL for Different Views of the Same Data
An operations manager’s dashboard may need package status, customer, driver and recent events. But another screen may need a different slice of the same model.
An operations screen might ask for:
package(id: "123") {
status
customer { name }
driver { name }
recentEvents { type time }
}
This is basically saying: “Give me package 123, but specifically return its status, the customer’s name, the driver’s name, and the recent event types and times.”
A representative response might look like:
{
"data": {
"package": {
"status": "Out for delivery",
"customer": { "name": "Jane Smith" },
"driver": { "name": "Luis Garcia" },
"recentEvents": [
{ "type": "Loaded", "time": "10:14 AM" },
{ "type": "Out for delivery", "time": "11:02 AM" }
]
}
}
}
A customer-facing tracking page might ask for much less:
package(id: "123") {
status
estimatedDelivery
}
And the response is correspondingly smaller:
{
"data": {
"package": {
"status": "Out for delivery",
"estimatedDelivery": "3:30 PM"
}
}
}
Same underlying package. Different requested shape.

That word “shape” just means the client can ask for the particular fields and related information it needs rather than always receiving one predetermined package response. REST can be designed to offer some of that flexibility too, but GraphQL makes client-selected fields and relationships a central part of the interface.
For a less technical PM, the useful mental model is: “I know the underlying information I need, but different screens or consumers may want different combinations of it.”
The PM question is whether your clients genuinely have different enough data needs for that flexibility to justify the added schema, authorization, performance and governance work.
gRPC When Two Services Need a Tight Contract
Let’s do an example of some behind-the-scenes needs. Let’s say our service needs to ask one internal service for thousands of route estimates. Other merchants and customers don’t need to, don’t want to, and will never see this exchange. When you use mapping software, do you need to see the piles of routes considered? No. You just want fastest or shortest based on your preferences. Okay, maybe every so often you’ll ask for a detour. But no, you don’t need all the options, just the answer. In any case, per gRPC intro, “In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services.”
So a route planner might make a call that looks like:
EstimateRoute(
start = "Warehouse A",
destination = "Customer B",
vehicleType = "delivery van"
)
and receive a defined response such as:
distanceMiles: 42.7
estimatedMinutes: 55
tollCost: 3.25

With gRPC, engineering can formally define that remote method, including the expected request and response fields and their data types. Client and server code can then be generated from that shared definition. That’s useful when several services, perhaps written in different programming languages, need to agree very precisely on how they communicate. gRPC can also support streaming when the use case requires it.
For a less technical PM, the important contrast is this: REST often feels like “request this resource at this URL.” RPC feels more like “call this defined function on another system.”
The PM question is not “Should I pick gRPC?” It is more likely: Is this a tightly controlled service-to-service relationship where efficiency and a strongly defined contract matter more than broad public familiarity?
A Webhook When You Want the Other System to Tell You
Suppose a merchant wants to know when package 123 is delivered.
One option is to ask over and over:
GET /packages/123
Every minute: “Delivered yet?” “Delivered yet?” “Delivered yet?” like a child asking if we’re there yet.
This is polling. It works, but you are repeatedly asking a question whose answer is usually still “no.”
A webhook flips that relationship around. The merchant effectively says, “Here is the internet address where you can reach my system. Tell me when this event happens.” It might subscribe to an event such as:
package.delivered
When the delivery occurs, ScottsHouseOfLogistics sends an HTTP request to the merchant’s specified callback address with event data such as:
{
"event": "package.delivered",
"packageId": "123",
"deliveredAt": "3:22 PM"
}
The merchant’s system might simply acknowledge receipt:
{
"accepted": true
}

The meaningful information here is traveling from ScottsHouseOfLogistics to the merchant. The response is just confirmation that the notification was received. The merchant no longer needs to keep checking.
An everyday analogy is the difference between walking to the front door every minute to see whether someone is there versus having a doorbell. The doorbell does not continuously report what is happening outside. It alerts you when the particular event you care about occurs.
For product, that creates a surprisingly real set of decisions. Which events can partners subscribe to? What information is included? What happens if the receiving system is temporarily down? Do we retry? How does the recipient recognize a duplicate delivery? How does a partner test all of this before going live?
For a less technical PM, the useful mental model is: “Don’t make me keep asking. Tell me when the thing happens.”
The PM question is: Does the consumer need information only when an event happens, rather than repeatedly asking whether something changed?
Asynchronous Messaging When the Producer Should Not Wait
Now suppose a package is scanned at a warehouse. Several things may care about that event. The customer-notification service may want to update the tracking page. Analytics may want the scan for operational reporting. A fraud or exception service may look for something unusual. Inventory systems may update their own state. In AsyncAPI, “A server acts as a messaging broker system, establishing connections and facilitating communication between producers and consumers. Unlike traditional API servers that rely on request-response interactions, message broker interactions occur bidirectionally across various channels.” (See AsyncAPI Server.)
The scanning system does not necessarily need to call all of those systems one by one and wait for each to finish. Instead, it can publish an event such as:
{
"event": "package.scanned",
"packageId": "123",
"facility": "Stamford Hub",
"time": "11:02 AM"
}
That event can go to a topic or message broker, where multiple consumers can process it independently:
package.scanned
├─> Customer Notification Service
├─> Analytics Pipeline
├─> Exception / Fraud Monitor
└─> Inventory System

This differs from the webhook example in an important way. The producer is not necessarily notifying one specific external callback and waiting for an acknowledgment. It is publishing work or information into an asynchronous system so other consumers can react in their own time.
The same idea shows up heavily in IoT and connected-device environments. A warehouse scanner, vehicle sensor or industrial device may publish readings or events without knowing every downstream consumer. MQTT is one lightweight publish/subscribe approach often used when device resources or network connectivity are constrained. Kafka and other messaging/event platforms are common in larger backend environments.
For a less technical PM, the useful mental model is: “Put the event into the system and let the interested consumers handle it without making the producer wait for all of them.”
The PM question is: Should the producer and consumers be decoupled so work can happen asynchronously, independently, or at different speeds?
A Stream When the Updates Keep Coming
A webhook is great for “the package was delivered.” A dispatcher looking at a live driver map has a different problem. Driver locations may change every few seconds.
Imagine this sequence:
10:31:02 Driver 27 → Main St.
10:31:07 Driver 27 → Oak Ave.
10:31:12 Driver 27 → Park Rd.
10:31:17 Driver 27 → Elm St.
In this case, that continuing sequence is the output. There may be no single final response until the connection ends.

Those are not really four unrelated business events from the user’s point of view. They are an ongoing flow of changing state.
A webhook is more like a doorbell. A stream is more like turning on a live radio broadcast. Once the connection is established, updates keep arriving until the session ends or the consumer disconnects.
That is where streaming approaches become useful. Server-Sent Events can continuously send updates in one direction, from server to client. WebSockets support an ongoing two-way connection, so both sides can send information without repeatedly opening new request-and-response exchanges.
The distinction matters because “realtime” is not one technical requirement. A stock ticker that only pushes prices toward a browser has different needs from a multiplayer application where both sides continuously send messages.
For a less technical PM, the useful mental model is: “The information keeps changing, so keep the flow open instead of treating every update as a separate conversation.”
The PM question is: Are we notifying someone that occasional things happened, or maintaining an ongoing conversation as information changes?
By the way, some of you might question steaming as I did. After all, isn’t the very core of the internet based on packets? If so, how can there be “streaming” at all. It’s really just semantics. Yes, the ‘net moves data in discrete packets. True. “Streaming,” however, is primarily an application- and transport-layer behavior, not a claim about continuous electromagnetic waves. So data may be generated and sent such that a receiver can begin consuming it, (such as playing a video), before the whole object gets there. The ‘continuous experience’ uses things like buffering and protocols designed for continuous delivery. So unlike a full download where a whole object has to be local before you can meaningfully use it, stream consumption starts earlier. I was originally thinking about it as a bit of a purist, but that’s not really useful. A better metaphor might be kind of like watching a movie. It’s a sequence of still frames, but it’s fast enough that our brain fills in the motion. Same deal with streaming. Mostly. Until we get the spinning beachball for too long. Anyway, back to our story.
MCP When an AI Needs to Find and Use Capabilities
Now suppose a customer tells an AI service assistant:
“My package is going to my old address. If it hasn’t gone out yet, change it to my office.”
The user has described the outcome they want, but they have not told the software exactly which backend operation to call.
The assistant may have several capabilities available:
find_package
estimate_delivery
change_delivery_address
hold_package
Think of this as giving the AI a labeled toolbox. The tools are not just sitting there with mysterious names. An MCP server can describe what each one does, what information it expects, and how it can be used. The AI application can inspect that available set, interpret the user’s request, and choose an appropriate capability.
For example, it may first use find_package to determine whether the package has already gone out:
Tool: find_package
Input: packageId = "123"
Result: status = "At warehouse"
Because the package has not gone out, it may then choose change_delivery_address:
Tool: change_delivery_address
Input: packageId = "123", address = "500 Office Plaza"
Result: delivery address updated

The user did not have to know either function existed.
The underlying business capability may still call the exact same API your web application uses. MCP does not magically replace your business logic. It provides a standardized way to expose tools and other context to an AI application.
That moves the PM discussion into new territory. Can the agent only read delivery information, or can it change something? Does an address change require human confirmation? What should the user see before and after a tool is invoked? How do we know later what action the AI actually took?
For a less technical PM, the useful mental model is: “Here are approved capabilities. Let the AI determine which one fits the user’s request.”
The PM question is: Do we need an AI application to discover and use capabilities based on a user’s goal rather than having every invocation predetermined in application code?
A2A When You Delegate Work, Not Just a Tool Call
Now suppose package 123 has not moved for forty-eight hours.
There may be no single function called:
fix_stalled_package()
Resolving the problem could mean checking several systems, contacting a carrier, waiting for a response, deciding whether to resend the item, asking the customer a question, and possibly escalating to a human.
This is less like handing someone a specific tool and more like giving a case to a specialist.
Instead of saying, “Run this exact function,” a service agent might hand a specialist logistics agent a task:
Investigate why package 123 is stalled, determine the next appropriate action, and return a resolution or escalation request.
The specialist agent may need several steps to complete that work. It may make progress, wait for something, continue later, return a result, or decide that a human needs to step in. The requesting agent does not necessarily need to know how every internal step is performed.
A representative result could be something like:
Investigation result:
- Last carrier scan was 48 hours ago.
- No subsequent movement was found.
- Replacement shipment is recommended.
- Human approval is required before reshipping.

The important point is that the result is an outcome from a delegated task, not merely the return value from one narrowly defined function.
That is a different unit of interaction. The first agent is delegating an outcome-oriented piece of work and allowing another independent agent to manage some of the steps required to complete it.
One useful way to remember the difference is:
MCP: “Here are tools you can use.”
A2A: “Here is work I need another agent to handle.”
That is simplified, but it captures the product-level distinction. An A2A agent might itself use MCP tools underneath while completing the delegated task. The two approaches can be complementary.
For a less technical PM, the useful mental model is: “I am not just exposing a function. I am handing responsibility for a task to another capable system.”
The PM question is: Are we exposing a capability to an agent, or asking another independent agent to take responsibility for a larger task or outcome?
One Product Can Need All of Them
This is the point of using one logistics product throughout the examples.
ScottsHouseOfLogistics might reasonably use:
- REST for straightforward merchant package lookups.
- GraphQL for internal screens with different combinations of related data.
- gRPC between high-volume internal services.
- Webhooks to tell merchants when delivery events occur.
- Asynchronous messaging to fan package scans or other events out to multiple downstream consumers.
- Streaming for a live dispatcher map.
- MQTT (Message Queuing Telemetry Transport) or similar messaging in some connected-device or vehicle-telemetry contexts.
- MCP to let an AI assistant discover and use approved capabilities.
- A2A when one independent agent delegates a longer-running task to another.
That is not a pile of competing architectures bolted onto one product for fun. Each can serve a different relationship.
The useful PM skill is recognizing the interaction first: Who is talking? Who initiates? Is this a request, an event, an asynchronous message, a flow of updates, a tool invocation or a delegated task? How much control or autonomy does the other side have? Once those questions are clear, a discussion with engineering about the implementation becomes much more productive.
What the Companion Code Should Prove
The companion Google Colab Notebook walks through the major interaction patterns with runnable examples. They don’t all use the fictional ScottsHouseOfLogistics service because I’m prioritizing free public APIs and self-contained demos that readers can actually run without accounts or paid API keys. The business domain may change from example to example, but the interaction pattern is the point.
Every example should answer the same questions: What problem are we solving? Why does this pattern fit? What does the interaction look like? What should a PM notice? What gets harder in production?
The code is not there to prove that a product leader can cosplay as an engineer. It is there to make the abstractions testable. Run the examples, watch the different conversations happen, and the taxonomy should start to feel like a working mental model rather than a glossary.