What Is COM? - Why the Design of Windows COM Is Still Beautiful Today

· Updated: · · 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.

Why the design of COM is beautifulDiagram 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 .NETusesusesusesusesusesusesimplementsrequiresimplementsrequiresusesusesrequiresmay causepreventsmay causeusesusesusesusesusesusesrequiresCOM (Component Object Model)IUnknownQueryInterfaceCOM reference counting (AddRef/Release)CLSID (Class ID)IID (Interface Identifier)HRESULTCOM binary compatibilityCOM interface versioningIn-Proc COM (DLL Server)COM LocalServer (out-of-process server)Bitness Match RequirementIn-Proc Crash Propagation to HostOut-of-Proc COM Disconnection ErrorsProxy/StubIDL (Interface Definition Language).NET COM InteropProgID (Programmatic Identifier)ActiveX

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.

Implementation - language does not matterCaller side - language does not matterComponent written in C++Component written in C#C++ appC# appVBA / Python and so onContract (the part fixed in binary form)- The IID (GUID) decides uniquely which contract this is- A method order that starts with the three IUnknown methods- Argument and return types plus calling convention per method

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 AddRef yourself. Both CoCreateInstance and QueryInterface have already called AddRef on the pointer they return. The rule, then, is that whoever receives a pointer calls Release on it, and you call AddRef explicitly only when a second place starts holding that same pointer.
  • A failed QueryInterface is 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.
Reference counting and the Release ruleA 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.Call AddRef explicitlyCoCreateInstance and QueryInterfaceA pointer that has already been AddRefd is returnedWhoever received it uses itWhoever received it calls ReleaseDestroyed once the reference count reaches 0A 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.

How C# code maps onto COMA 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.C# codeCast to a typeReleaseComObjectFailing HRESULTEquivalent to QueryInterfaceEquivalent to ReleaseConverted into an exceptionThe 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.

QueryInterface(IID_ICalcService)S_OKQueryInterface(IID_ICalcService)S_OK - not broken by the updateQueryInterface(IID_ICalcServiceEx)S_OK - new features availableQueryInterface(IID_ICalcServiceEx)E_NOINTERFACE - carry on with the old featuresOld callerknows only ICalcServiceNew callerasks for ICalcServiceExOld version of the componentimplements only ICalcServiceNew version of the componentimplements both

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.

The two places a component can liveA 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.Calling code (looks the same either way)In-proc (DLL server)Out-of-proc (EXE server)Loaded as a DLL into the same processStarted 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.

What happens when the other process disappearsA 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.Server process crashes or exitsThe call returns as a failureThe caller survivesOn to restart and reconnect recoveryNot 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.

The center of COM designA 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.Language-neutral designIndependence from language, process, and implementationUnique identification through GUIDsIUnknown reference countingTransparent interprocess communication

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.

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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).

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog