What Is COM? - Why the Design of Windows COM Is Still Beautiful Today
· Updated: · Go Komura · COM, ActiveX, Windows Development
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614443)
- First published
Cite this article(DOI: 10.5281/zenodo.21614442)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). What Is COM? - Why the Design of Windows COM Is Still Beautiful Today. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614442 https://comcomponent.com/en/blog/2026/01/25/001-why-com-is-beautiful/
- DOI (latest version)
- 10.5281/zenodo.21614442
- DOI (this version)
- 10.5281/zenodo.22217114
What Is COM?
COM (Component Object Model) is a “binary contract” that lets components talk to each other on Windows. It is a mechanism for communicating through interfaces as strict contracts, across differences in language and compiler, and at its core lies the design philosophy of “programming against the contract, not the implementation.”
Knowledge map for this article
COM is a binary contract that lets components interact with each other on Windows, and IUnknown, which every interface inherits, provides capability discovery through QueryInterface and reference count management through AddRef/Release. Identifying components and interfaces uniquely with the GUIDs called the CLSID and the IID avoids name collisions, and the design of adding a new interface without changing the existing one and checking for it with QueryInterface is what makes versions able to coexist. In-proc (a DLL server), which is loaded into the same process as the caller, requires matching bitness and goes down together with the other side when it crashes, whereas Out-of-proc (a LocalServer), which runs in a separate process, is free of that constraint but in exchange has to be prepared for disconnection errors such as RPC_E_DISCONNECTED when the other side disappears. Expressing success and failure through the HRESULT return value, together with Proxy/Stub and contracts defined up front in IDL, is what makes the whole mechanism independent of language and process boundaries.
flowchart LR
accTitle: Why the design of COM is beautiful
accDescr: Diagram showing how COM builds on reference counting and QueryInterface provided by IUnknown and on identification through CLSID and IID to deliver binary compatibility, interface separation, version coexistence, and reuse across process boundaries, and how that connects to the In-proc and Out-of-proc deployment forms, to HRESULT, and to interoperability with .NET
com["COM (Component Object Model)"]
iunknown["IUnknown"]
queryinterface["QueryInterface"]
com_reference_counting["COM reference counting (AddRef/Release)"]
clsid["CLSID (Class ID)"]
iid["IID (Interface Identifier)"]
hresult["HRESULT"]
com_binary_compatibility["COM binary compatibility"]
com_interface_versioning["COM interface versioning"]
in_proc_com["In-Proc COM (DLL Server)"]
com_localserver["COM LocalServer (out-of-process server)"]
bitness_match_requirement["Bitness Match Requirement"]
in_proc_crash_propagation["In-Proc Crash Propagation to Host"]
out_of_proc_com_disconnection_error["Out-of-Proc COM Disconnection Errors"]
proxy_stub["Proxy/Stub"]
idl["IDL (Interface Definition Language)"]
dotnet_com_interop[".NET COM Interop"]
progid["ProgID (Programmatic Identifier)"]
activex["ActiveX"]
com -->|"uses"| iunknown
com -->|"uses"| queryinterface
com -->|"uses"| com_reference_counting
com -->|"uses"| clsid
com -->|"uses"| iid
com -->|"uses"| hresult
com -->|"implements"| com_binary_compatibility
com_binary_compatibility -->|"requires"| iunknown
com -->|"implements"| com_interface_versioning
com_interface_versioning -->|"requires"| queryinterface
com -.->|"uses"| in_proc_com
com -.->|"uses"| com_localserver
in_proc_com -->|"requires"| bitness_match_requirement
in_proc_com -->|"may cause"| in_proc_crash_propagation
com_localserver -->|"prevents"| in_proc_crash_propagation
com_localserver -.->|"may cause"| out_of_proc_com_disconnection_error
com -.->|"uses"| proxy_stub
com -->|"uses"| idl
dotnet_com_interop -.->|"uses"| progid
dotnet_com_interop -->|"uses"| queryinterface
dotnet_com_interop -->|"uses"| hresult
dotnet_com_interop -->|"uses"| com_reference_counting
activex -->|"requires"| com
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (23 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
The Three Key Elements of COM
What this chapter lists are the parts that make up the mechanism called COM. The four strengths of COM that follow are properties you get as a result of combining those parts, so this is not the same explanation given twice. A table at the end of the strengths chapter shows how the two line up.
1. Interface-Centric Design
In COM, “the contract comes before the implementation.” You can use an object without knowing anything about its internal implementation, as long as you know its published interfaces.
2. Identification by GUIDs (CLSID / IID)
Every component and interface is assigned a globally unique ID (GUID), so name collisions simply cannot happen.
3. IUnknown
The base interface that every COM interface inherits from. It provides the following three functions.
| Method | Role |
|---|---|
QueryInterface |
Ask whether the object supports another interface |
AddRef |
Increment the reference count |
Release |
Decrement the reference count (the object destroys itself when it reaches 0) |
Once these three elements come together, all that is left between the caller and the implementation is an interface identified by a GUID whose method order is fixed. Neither the language nor the compiler shows up anywhere in that contract.
flowchart LR
subgraph CALLER["Caller side - language does not matter"]
A1["C++ app"]
A2["C# app"]
A3["VBA / Python and so on"]
end
CONTRACT["Contract (the part fixed in binary form)<br/>- The IID (GUID) decides uniquely which contract this is<br/>- A method order that starts with the three IUnknown methods<br/>- Argument and return types plus calling convention per method"]
subgraph IMPL["Implementation - language does not matter"]
B1["Component written in C++"]
B2["Component written in C#"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
Figure 1: COM’s binary contract. Neither the caller’s language nor the implementation’s language appears in the contract, so either side can be swapped without rebuilding the other
“Programming Against the Contract” Seen in Code
Prose alone makes this hard to picture, so here is the smallest amount of code that shows it: using ICalcService, a component that does nothing but add two numbers, without knowing anything about its implementation.
Start with C++ (raw COM). The definition of ICalcService written here is the contract itself, and whether the implementation is written in C++ or in C# appears nowhere in the calling code.
#include <objbase.h>
// The contract definition. Normally a header in this shape is generated from IDL
// It inherits from IUnknown, so it always has QueryInterface / AddRef / Release
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};
// An extended contract added later. It gets a different GUID
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
// CLSID of the implementing component. Normally defined in a header generated from IDL
static const CLSID CLSID_CalcService =
{ 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };
// Caller
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }
ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
__uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// The pointer returned here has already been AddRef'd (reference count is 1)
if (SUCCEEDED(hr))
{
int sum = 0;
hr = calc->Add(1, 2, &sum); // Callable without knowing the implementation
// Ask at run time whether the extended contract is supported too = version coexistence in practice
ICalcServiceEx* calcEx = nullptr;
if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
reinterpret_cast<void**>(&calcEx))))
{
// Reached only when the component is a newer version
calcEx->Release(); // Whoever received it releases it
}
// If it is not supported, only E_NOINTERFACE comes back and the old version keeps working
calc->Release(); // The reference count drops to 0 and the object is destroyed
}
CoUninitialize();
There are three things worth noticing.
- You will rarely write
AddRefyourself. BothCoCreateInstanceandQueryInterfacehave already calledAddRefon the pointer they return. The rule, then, is that whoever receives a pointer callsReleaseon it, and you callAddRefexplicitly only when a second place starts holding that same pointer. - A failed
QueryInterfaceis not an error condition. “That contract is not supported” (E_NOINTERFACE) is a perfectly normal answer, and it is what makes it possible to add new functionality without breaking old components. - Every return value is an
HRESULT. Reporting success or failure through a return value rather than an exception is a convention that crossing language boundaries requires. Every language throws exceptions differently, but an integer return value is something anyone can interpret.
flowchart TB
accTitle: Reference counting and the Release rule
accDescr: A diagram showing that CoCreateInstance and QueryInterface return pointers that have already been AddRefd, so whoever receives one calls Release when finished, and the object is destroyed once the reference count reaches zero.
api["CoCreateInstance and QueryInterface"] --> ret["A pointer that has already been AddRefd is returned"]
ret --> use["Whoever received it uses it"]
use --> rel["Whoever received it calls Release"]
rel --> zero["Destroyed once the reference count reaches 0"]
use -.->|"Call AddRef explicitly"| add["A second place holds the same pointer"]
Figure 2: The rule is that whoever receives a pointer calls Release, and AddRef is called explicitly only when adding another place that holds it.
Using the same contract from C# looks like this. The same GUID means the same contract, so it does not matter that the other side is a C++ implementation.
using System;
using System.Runtime.InteropServices;
// Write the same GUID as the C++ side. This is what declares it the same contract
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// Caller
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
?? throw new InvalidOperationException("The COM server is not registered.");
object server = Activator.CreateInstance(t)!;
try
{
var calc = (ICalcService)server; // This cast is the equivalent of QueryInterface
int sum = calc.Add(1, 2);
Console.WriteLine(sum); // 3
}
finally
{
Marshal.ReleaseComObject(server); // The equivalent of Release
}
Add returns an int on the C# side because .NET COM interop applies a fixed transformation: it turns the last [out, retval] argument into the return value and converts a failing HRESULT into an exception. AddRef and Release also disappear from the caller’s view, but they have not gone away - the thin wrapper .NET provides (the RCW) calls them instead. The same contract, used in whatever way is natural for each language - that is what a binary contract actually looks like.
flowchart TB
accTitle: How C# code maps onto COM
accDescr: A diagram showing that in C# a cast to an interface corresponds to QueryInterface, Marshal.ReleaseComObject corresponds to Release, a failing HRESULT is converted into an exception, and the RCW calls AddRef and Release on the caller's behalf.
cs["C# code"] --> cast["Cast to a type"]
cs --> rco["ReleaseComObject"]
cs --> hrx["Failing HRESULT"]
cast --> qi["Equivalent to QueryInterface"]
rco --> rel["Equivalent to Release"]
hrx --> ex["Converted into an exception"]
cs -.-> rcw["The RCW handles reference counting"]
Figure 3: The contract is the same, but on the C# side it turns into whatever is natural for the language, with the RCW and the interop layer handling the conversion.
The Four Strengths of COM
1. Binary Compatibility
A component you build once can be reused regardless of programming language or runtime. Calling a COM component written in C++ from C# or Python is an ordinary thing to do.
2. Interface Separation
Because the implementation is hidden completely and only the contract is published, you can change the internals freely without affecting callers.
3. Version Coexistence
The basic design for adding functionality while preserving backward compatibility is to add new interfaces. New features can be offered without changing the old interface.
Combine old and new callers with old and new components and three of the four pairings work as they are, while the fourth simply gets back the answer that the contract is not supported.
flowchart LR
OLDC["Old caller<br/>knows only ICalcService"]
NEWC["New caller<br/>asks for ICalcServiceEx"]
OLDS["Old version of the component<br/>implements only ICalcService"]
NEWS["New version of the component<br/>implements both"]
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK"| OLDS
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK - not broken by the update"| NEWS
NEWC -->|"QueryInterface(IID_ICalcServiceEx)<br/>S_OK - new features available"| NEWS
NEWC -.->|"QueryInterface(IID_ICalcServiceEx)<br/>E_NOINTERFACE - carry on with the old features"| OLDS
Figure 4: Adding a new IID without changing the existing interface keeps every combination of old and new working. The dotted line is the normal answer saying that contract is not supported
4. Reuse Across Process Boundaries
There are two places a COM component can live: in-proc (a DLL server), loaded as a DLL into the same process as the caller, and out-of-proc (an EXE server, also called a LocalServer), started as an EXE in a separate process. The calling code looks the same either way.
flowchart TB
accTitle: The two places a component can live
accDescr: A diagram showing that calling code which looks the same either way can reach an in-proc component loaded as a DLL into the caller process or an out-of-proc component started as an EXE in a separate process.
caller["Calling code (looks the same either way)"] --> ip["In-proc (DLL server)"]
caller --> op["Out-of-proc (EXE server)"]
ip -.-> ipd["Loaded as a DLL into the same process"]
op -.-> opd["Started as an EXE in a separate process"]
Figure 5: There are two hosting options, in-proc and out-of-proc, but the calling code looks the same.
| In-proc (DLL server) | Out-of-proc (EXE server) | |
|---|---|---|
| Where it runs | The same process as the caller | A separate process |
| What the call really is | A direct call through a function pointer | Arguments are repacked (marshaling) and passed over interprocess communication |
| Speed | Fast | Slower by the cost of interprocess communication |
| If the other side goes down | The caller goes down with it | The caller survives (the call returns as a failure) |
| Bitness (32/64-bit) | Must match, or the DLL cannot be loaded | May differ |
With out-of-proc COM (an EXE server) you can call into another process’s functionality safely. That last row, where 32-bit and 64-bit are allowed to differ, pays off in real work, and the concrete way to use a 64-bit DLL from a 32-bit app is covered in “A Worked Example of a COM Bridge for Calling a 64-bit DLL from a 32-bit App.”
Being in a separate process also means the other side can disappear at any moment. When the server process crashes or exits, the caller gets failures like the ones below. These are out-of-proc-specific errors that say the other side is gone, not that something is broken.
| Error code | Meaning |
|---|---|
RPC_E_DISCONNECTED |
The object being called has been disconnected from the client (the object on the other side no longer exists) |
RPC_S_SERVER_UNAVAILABLE |
The server process being called cannot be reached |
In-proc, none of this needed any thought, because if the other side went down so did you. Out-of-proc, it enters the design as recovery logic for restarting and reconnecting. The accurate way to put it is that this is extra work taken on in exchange for safety.
flowchart TB
accTitle: What happens when the other process disappears
accDescr: A diagram showing that out-of-proc, when the server process disappears through a crash or an exit, the call returns as a failure and the caller survives, so restart and reconnect recovery logic has to be part of the design.
gone["Server process crashes or exits"] --> fail["The call returns as a failure"]
fail --> alive["The caller survives"]
alive --> rec["On to restart and reconnect recovery"]
fail -.-> mean["Not broken, the other side is simply gone"]
Figure 6: Out-of-proc, you do not go down with the other side, but recovery logic becomes part of the design.
How the Three Elements Map to the Four Strengths
As noted at the start, the three elements are the parts and the four strengths are what they produce. Lining up which part supports which strength makes the division of labor clear where the two looked like they overlapped.
| Strength | Elements mainly at work |
|---|---|
| 1. Binary compatibility | Interface-centric design + IUnknown (the call order is fixed at the binary level) |
| 2. Interface separation | Interface-centric design (only the contract is published) |
| 3. Version coexistence | GUIDs + QueryInterface (another contract can be asked for at run time) |
| 4. Reuse across process boundaries | Interface-centric design (even the location of the implementation is separated from the contract) |
COM Is Still in Active Use
COM tends to be written off as old technology, but it is a mechanism that remains in use at the very core of Windows.
Where COM Shows Up
- Explorer extensions (context menus, preview handlers)
- Office automation (driving Excel and Word from outside)
- Interoperability with .NET (COM Interop)
- Existing systems, including ActiveX
- DirectX, the Windows Shell API, and many other Windows APIs
Even if you assume none of this concerns you, COM shows up somewhere as long as you develop for Windows.
Summary
At the center of COM’s design is independence from language, process, and implementation: language-neutral interface design, unique identification and version management through GUIDs, reference counting through IUnknown, and a mechanism that handles interprocess communication transparently.
flowchart TB
accTitle: The center of COM design
accDescr: A diagram showing that four mechanisms, language-neutral interface design, unique identification through GUIDs, reference counting through IUnknown, and transparent interprocess communication, together support independence from language, process, and implementation as the center of the design.
i1["Language-neutral design"] --> core["Independence from language, process, and implementation"]
i2["Unique identification through GUIDs"] --> core
i3["IUnknown reference counting"] --> core
i4["Transparent interprocess communication"] --> core
Figure 7: Four mechanisms come together to support the center of COM, independence from language, process, and implementation.
Calling a design beautiful is a subjective way to put it, but the grounds for saying so are concrete. Line up the problems COM set out to solve against the way it solved them and you get this.
| Constraint that existed then (and still does) | COM’s answer |
|---|---|
| C++ has no standard binary convention (ABI), so a different compiler alone is enough to make reuse impossible. Neither name mangling nor object layout lines up | Make only the order of entries in the virtual function table a convention. Narrowing it to that single point is what made language and compiler irrelevant |
| Replacing a library forces a rebuild of everything that uses it | No rebuild is needed as long as the contract (the interface) stays the same. The binary can be swapped in as it is |
| You want to add functionality but cannot break existing callers | Add an interface instead of changing one, and use QueryInterface to ask at run time whether it is supported |
| Names collide (two different things with the same class name have to live side by side) | Identify uniquely by GUID. The need to negotiate names disappeared entirely |
| Calling into another process or another machine changes how the call has to be written | Insert a proxy/stub and keep the calling code in the same shape |
In other words, COM’s design did not start from the question of what would look elegant. It arrived at its present shape as the result of clearing away, one at a time, the concrete obstacles that were blocking reuse. Designs where the mapping from constraint to solution can be traced this straightforwardly are not common.
And that way of solving things carries over intact into modern component-oriented development.
| COM | Modern counterpart |
|---|---|
| Settle the contract first in IDL and generate the code for both sides from it | Generate client and server from an OpenAPI or Protocol Buffers schema |
| Add new interfaces rather than changing existing ones | Add fields in Protocol Buffers without reusing field numbers, and version the API |
| Implementation language does not matter (a binary contract) | Implementation language does not matter (a message contract over the network) |
| A proxy/stub hides the process boundary | An RPC client stub hides the network boundary |
The only differences are the kind of boundary (a process boundary on one machine, or a network) and how the contract is expressed (binary, or text and schemas). Understanding COM is what makes it click, when you design interfaces between microservices, why the contract is settled first and why existing fields must not be deleted - not as a fashion, but as a necessity.
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Windows App Outsourcing and Custom Software Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or custom software development, here is how to sort out existing software modification, devi...
A Developer's Strange Love, or: How I Learned to Stop Worrying and Love Windows
Windows is a hassle. But that hassle is the hassle of an OS that has carried real-world business on its back.
Registration and Bitness Pitfalls in COM/OCX/ActiveX Development
A practical look at the 32bit/64bit, Visual Studio 2022, regsvr32/Regasm, administrator-rights, HKCR, and STA/MTA pitfalls that trip up C...
What Are COM / ActiveX / OCX? - The Differences and Relationships Explained
A practical guide to what COM is, what ActiveX is, and what OCX is - covering their differences and relationships, the connection to OLE,...
How to Handle ActiveX / OCX Today - A Keep / Wrap / Replace Decision Table
When you find ActiveX / OCX, how to choose between keeping, wrapping, and replacing it, covering 32-bit / 64-bit, registration, browser d...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
Where This Topic Connects
This article connects naturally to the following service pages.
Legacy Asset Reuse & Migration Support
Understanding COM's design and compatibility model is a natural entry point for thinking about how to make the most of existing Windows assets.
Technical Consulting & Design Review
If you want to sort out your strategy with an understanding of IUnknown, GUIDs, and boundary design, that leads naturally into technical consulting and design review.
Frequently Asked Questions
Common questions about the topic of this article.
- What is COM?
- COM (Component Object Model) is a binary contract that lets components talk to each other on Windows. It is a mechanism for communicating through interfaces as strict contracts, across differences in language and compiler. Underneath it lies the design philosophy of programming against the contract, not the implementation.
- What is IUnknown?
- IUnknown is the base interface that every COM interface inherits from. It provides three functions: QueryInterface, which asks whether the object supports another interface; AddRef, which increments the reference count; and Release, which decrements the reference count and destroys the object once it reaches zero. COM object lifetime management rests entirely on this reference count.
- Is COM still used today?
- Yes. COM is a mechanism that remains in use at the very core of Windows today. It shows up in Explorer extensions (context menus and preview handlers), Office automation for Excel and Word, COM Interop with .NET, existing systems including ActiveX, and many other places such as DirectX and the Windows Shell API. As long as you develop for Windows, COM will show up somewhere.
- What are the strengths of COM?
- There are four main ones: binary compatibility, which lets a component you built once be reused regardless of language or runtime; interface separation, which hides the implementation and publishes only the contract; version coexistence, which preserves backward compatibility by adding new interfaces; and safe calls into functionality in another process through out-of-proc COM (EXE servers).