DEV Community

Artifizer
Artifizer

Posted on

APIs Are Well Engineered. What About Everything Else?

Software engineers have learned how to engineer APIs quite well.

For an API, we routinely think about:

  • contracts
  • schemas
  • versioning
  • backward compatibility
  • breaking changes
  • authentication
  • authorization
  • ownership
  • documentation
  • discovery
  • lifecycle and deprecation

If I expose:

POST /customers
Enter fullscreen mode Exit fullscreen mode

I can define exactly:

  • what the request looks like
  • what the response looks like
  • who can call it
  • which version is being used
  • whether a change breaks existing clients

We have OpenAPI, OAuth, API gateways, schema validation, versioning rules, compatibility tools, and mature practices around all of this.

But modern software is no longer made only of APIs.

We also exchange and operate on:

  • events
  • configuration objects
  • user and tenant settings
  • workflows
  • serverless function contracts
  • MCP tools
  • AI agents
  • prompts
  • policies
  • extension manifests
  • plugin-defined data

These artifacts are contracts too.

But they are often still handled by separate, ad-hoc mechanisms.


Imagine you are designing an event platform

Assume the platform is not just a REST API.

Events may arrive through:

  • gRPC
  • Kafka
  • WebSockets
  • internal queues/SDKs

So we cannot simply say:

OpenAPI validates everything for us.

The event system itself needs to understand the contracts.

Suppose the platform has events like:

Event
├── Application X Activated
│
├── MCP Agent Y
│   ├── Work Started
│   └── Work Completed
│
└── Audit Event
    ├── User Authentication Failure
    └── User Logged In
Enter fullscreen mode Exit fullscreen mode

The platform itself defines:

Event
Audit Event
User Authentication Failure
User Logged In
Enter fullscreen mode Exit fullscreen mode

while external applications, agents and vendors may introduce their own event types, e.g.:

MCP Agent Y -> Work Started
Enter fullscreen mode Exit fullscreen mode

Let's consider a common Event

Every event should contain common fields:

{
  "timestamp": "...",
  "tenantId": "...",
  "eventType": "...",
  "payload": "..."
}
Enter fullscreen mode Exit fullscreen mode

Call this schema:

Event
Enter fullscreen mode Exit fullscreen mode

Now audit events need some additional common fields:

...
"payload": {
  "userId": "...",
  "ipAddress": "...",
  "auditData": "..."
}
Enter fullscreen mode Exit fullscreen mode

So we define:

Audit Event
Enter fullscreen mode Exit fullscreen mode

as a more specialized form of:

Event
Enter fullscreen mode Exit fullscreen mode

Then specific audit events add their own fields.

For example:

User Authentication Failure
Enter fullscreen mode Exit fullscreen mode

could additionally require:

...
"auditData": {
  "error": "Invalid password"
}
Enter fullscreen mode Exit fullscreen mode

while:

User Logged In
Enter fullscreen mode Exit fullscreen mode

does not need an error.

So the relationship is:

Event
  timestamp
  tenantId
  eventType

    ↓

Audit Event
  userId
  ipAddress

    ↓

User Authentication Failure
  error
Enter fullscreen mode Exit fullscreen mode

and:

Event
    ↓
Audit Event
    ↓
User Logged In
Enter fullscreen mode Exit fullscreen mode

The important idea is simple:

A more specific event inherits (or specifies) the contract of the more general event above it.

So a User Authentication Failure must satisfy:

Event fields
+
Audit Event fields
+
Authentication Failure fields
Enter fullscreen mode Exit fullscreen mode

Likewise, User Logged In must satisfy:

Event fields
+
Audit Event fields
+
User Logged In fields
Enter fullscreen mode Exit fullscreen mode

Here we can call this type derivation, by analogy with traditional programming languages.

It is similar to inheritance in programming languages, but applied to schemas and data exchanged between independent systems.


How do we express this relationship?

Programming languages solve this naturally.

You might write:

class Event:
    timestamp: datetime
    tenant_id: str

class AuditEvent(Event):
    user_id: str
    ip_address: str

class AuthenticationFailure(AuditEvent):
    error: str
Enter fullscreen mode Exit fullscreen mode

But now these types are not Python classes.

They are JSON objects traveling between different systems.

JSON Schema can describe each structure.

But we also want the platform to know:

Authentication Failure derives from Audit Event

Audit Event derives from Event
Enter fullscreen mode Exit fullscreen mode

That relationship becomes useful for much more than validation.


How should the event system validate an incoming event?

Suppose this arrives:

{
  "timestamp": "2026-10-03T10:00:00Z",
  "tenantId": "customer-12",
  "eventType": "user_authentication_failure",
  "userId": "42",
  "payload": {
      "ipAddress": "10.1.2.3",
      "auditData": {
           "error": "Invalid password"
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

The event system should be able to answer:

  • What type is this event?
  • Where is its schema?
  • Is that schema registered?
  • Does this object satisfy the schema?

Who can produce an event?

Now security appears.

Suppose:

Application X
Enter fullscreen mode Exit fullscreen mode

is allowed to produce:

Application X Activated
Enter fullscreen mode Exit fullscreen mode

Should it also be allowed to emit:

User Logged In
Enter fullscreen mode Exit fullscreen mode

Probably not.

Otherwise any application could impersonate the authentication subsystem simply by sending JSON with the correct fields.

We may want rules like:

Application X
    may produce
        Application X events

Authentication Service
    may produce
        Audit authentication events
Enter fullscreen mode Exit fullscreen mode

This is similar to API authorization.

With APIs where every resource type is explicit, we might say:

service A can call /billing/*
Enter fullscreen mode Exit fullscreen mode

For events, we want to say:

service A can produce this family of event types
Enter fullscreen mode Exit fullscreen mode

Who can read an event?

The same applies to consumers.

Perhaps a normal application can read:

- Application X Activated
- Job Started
- Job Completed
Enter fullscreen mode Exit fullscreen mode

but it should not see audit events.

An administrator might be allowed to read:

all Event types
Enter fullscreen mode Exit fullscreen mode

while an auditor might receive:

Audit Event
and everything derived from Audit Event
Enter fullscreen mode Exit fullscreen mode

This becomes especially useful as the system grows.

If tomorrow the platform adds:

- Password Changed
- API Token Created
- Administrative Permission Changed
Enter fullscreen mode Exit fullscreen mode

we don't want to rewrite every security policy.

They are all still:

Audit Event
Enter fullscreen mode Exit fullscreen mode

So a rule defined for the parent type can apply automatically to the new derived types.


What happens when the common Event contract changes?

Suppose millions of events have already been stored.

The original schema was:

Event v1

- timestamp
- tenantId
- eventType
Enter fullscreen mode Exit fullscreen mode

Later we decide that every event should also have origin as v1.1:

- origin
Enter fullscreen mode Exit fullscreen mode

Now the questions become much more practical:

  • What happens to all historical events that don't contain origin?
  • Are they still valid?
  • Should the new field be optional?
  • Can it later become required?
  • When an API returns an old event, which schema should it claim to follow?
  • Should the API transform old events and populate origin?
  • Can new consumers safely read both versions?
  • How long must the platform support v1 for ingest?
  • Which producers still send v1?
  • Can we automatically determine whether v1.1 is compatible with v1?

This is not very different from evolving an API.

But the artifact is an event type, not an API endpoint resource.

The same compatibility problem still exists.


What if Audit Event changes?

Now imagine adding:

- sessionId
Enter fullscreen mode Exit fullscreen mode

to Audit Event.

That change should affect:

- User Authentication Failure
- User Logged In
- Password Changed
Enter fullscreen mode Exit fullscreen mode

but it should not affect:

- Application X Activated
- MCP Agent Work Started
Enter fullscreen mode Exit fullscreen mode

So the platform needs to understand the hierarchy:

Event
├── Application X Activated
├── MCP Agent Work Started
└── Audit Event
    ├── User Authentication Failure
    └── User Logged In
Enter fullscreen mode Exit fullscreen mode

That allows it to answer:

  • Which schemas may be affected by this change?
  • Which stored data uses previous versions?
  • Which producers need to be upgraded?
  • Which consumers may break?

External vendors make this harder

Now imagine that the platform supports third-party applications.

The platform defines its core event hierarchy:

Event
└── Audit Event
    ├── User Authentication Failure
    └── User Logged In
Enter fullscreen mode Exit fullscreen mode

But Vendor B installs an integration and introduces:

Event
└── Integration B Connection Failed
Enter fullscreen mode Exit fullscreen mode

Vendor C installs an AI product and introduces:

Event
└── AI Agent C Started
Enter fullscreen mode Exit fullscreen mode

Both should still be valid platform events.

Conceptually:

Event
├── Application X Activated
├── Integration B Connection Failed
├── AI Agent C Started
└── Audit Event
    ├── User Authentication Failure
    └── User Logged In
Enter fullscreen mode Exit fullscreen mode

Now several new questions appear.

Who owns:

Integration B Connection Failed
Enter fullscreen mode Exit fullscreen mode

How do we prevent another vendor from registering a type with the same name?

  • Which application is allowed to emit it?
  • Can Vendor B extend one of the platform's common types?
  • What happens when Vendor B publishes version 2?
  • Which consumers accept version 1, version 2, or both?

This is where simple local names stop being sufficient.


Events are not special

Events are just an easy example.

The same problem appears with many other data types.

Consider platform-managed data such as:

- User Settings
- Subscription Settings
- VM Settings
- Application Settings
- Integration Settings
- ...
Enter fullscreen mode Exit fullscreen mode

The platform may want to provide:

storage
validation
versioning
access control
discovery
Enter fullscreen mode Exit fullscreen mode

while allowing installed applications and integrations to add their own attributes or specialized types.

One integration may define additional subscription settings.

Another vendor may introduce additional VM properties.

A plugin may add user-level settings.

You immediately get the same questions:

  • Who owns this data type?
  • What does it derive from?
  • Which version is stored?
  • Who can read it?
  • Who can modify it?
  • Can another vendor reference it?
  • Can an extension add fields without breaking existing consumers?
  • What happens to old stored objects after the schema evolves?

MCP tools have the same problem

Suppose an MCP tool declares:

Input:
    Repository

Output:
    Code Analysis
Enter fullscreen mode Exit fullscreen mode

What exactly is:

Repository
Enter fullscreen mode Exit fullscreen mode
  • Is it a local name?
  • A GitHub repository?
  • A generic source-code repository type?
  • Which vendor defines it?
  • Which version?

Can the tool accept a specialized subtype such as:

GitHub Repository
Enter fullscreen mode Exit fullscreen mode

Can it accept every type derived from:

Repository
Enter fullscreen mode Exit fullscreen mode

Then security questions appear:

  • Who is allowed to invoke this tool?
  • Which types of data may be passed into it?
  • May a third-party MCP tool receive sensitive configuration?
  • Which output types is it allowed to create?
  • Can its output be passed safely to another agent?

Again, these look surprisingly similar to the problems we already solved around APIs.


The missing common layer

APIs have:

identity
contracts
versions
compatibility
ownership
security
references
Enter fullscreen mode Exit fullscreen mode

But for many other artifacts we repeatedly create separate mechanisms:

event registry
schema registry
config registry
agent registry
MCP registry
function registry
workflow registry
Enter fullscreen mode Exit fullscreen mode

Each eventually needs some version of:

name
owner
schema
version
references
permissions
compatibility
Enter fullscreen mode Exit fullscreen mode

Perhaps these are not completely separate problems, and they need a shared type system underneath them.


This is the idea behind Global Type System

This is what we are exploring with Global Type System — GTS.

GTS is an open specification and open-source project for identifying and referencing data types and their instances.

The specification is language-independent, with JSON and JSON Schema as its primary current focus.

A GTS type identifier looks like:

gts.acme.billing.events.invoice_created.v1~
Enter fullscreen mode Exit fullscreen mode

Its structure is:

gts.<vendor>.<package>.<namespace>.<type>.v<major>[.<minor>]~
Enter fullscreen mode Exit fullscreen mode

For example:

vendor      = acme
package     = billing
namespace   = events
type        = invoice_created
version     = v1
Enter fullscreen mode Exit fullscreen mode

The trailing ~ identifies a type.

GTS can also identify instances of types.

The project is developed openly, and the specification repository includes its license and implementation rules.


Why put all of this into the identifier?

Because one identifier can carry several useful pieces of information.

1. Type or instance identity

A GTS identifier can identify:

a schema/type
Enter fullscreen mode Exit fullscreen mode

or:

a concrete instance of that type
Enter fullscreen mode Exit fullscreen mode

So the same identification model applies to both definitions and data.


2. Ownership

The identifier encodes:

vendor
package
namespace
type
Enter fullscreen mode Exit fullscreen mode

For example:

gts.vendor_b.integration.events.connection_failed.v1~
Enter fullscreen mode Exit fullscreen mode

and:

gts.vendor_c.ai.events.agent_started.v1~
Enter fullscreen mode Exit fullscreen mode

can coexist without colliding.

The ownership is visible directly from the identifier.


3. Versioning

Version is part of the type identity:

gts.acme.billing.events.invoice_created.v1~
gts.acme.billing.events.invoice_created.v1.1~
gts.acme.billing.events.invoice_created.v2~
Enter fullscreen mode Exit fullscreen mode

This gives tooling something deterministic to reason about when schemas evolve.


4. Type inheritance and derivation

GTS identifiers can be chained to represent that one type derives from another.

Conceptually:

Event
    ↓
Audit Event
    ↓
User Authentication Failure
Enter fullscreen mode Exit fullscreen mode

The derived type keeps the relationship to its parent types.

That means tooling can determine not only:

this is User Authentication Failure
Enter fullscreen mode Exit fullscreen mode

but also:

this is an Audit Event
this is also an Event
Enter fullscreen mode Exit fullscreen mode

This relationship can then be used for validation, discovery, subscriptions, or policy decisions.


5. Access control

Because identifiers have predictable namespaces, security policies can work with exact identifiers or wildcard patterns.

For example:

gts.acme.billing.events.*
Enter fullscreen mode Exit fullscreen mode

could represent all billing event types belonging to the package.

A policy system could therefore express rules such as:

Service A may produce:
    gts.acme.billing.events.*

User B may read:
    gts.acme.public.events.*

Auditor may read:
    Audit Event and its derived types
Enter fullscreen mode Exit fullscreen mode

The policy engine itself is separate from GTS.

GTS provides the structured identity on which the policy can operate.


6. References between data types

Types also need to refer to other types.

This is effectively the schema equivalent of a foreign key.

For example:

Invoice
    references
        Customer
Enter fullscreen mode Exit fullscreen mode

or:

Agent
    references
        Tool
Enter fullscreen mode Exit fullscreen mode

Instead of referencing a vendor-specific local schema name, the reference can point to a globally identified type.

That becomes important when different organizations publish schemas independently.


The larger idea

Programming languages gave us type systems inside applications.

API engineering gave us strong contracts, versions, compatibility rules, and security between services.

But modern platforms now exchange much more than API requests.

They exchange:

events
settings
configs
workflows
tool contracts
agent data
function definitions
policies
extension data
Enter fullscreen mode Exit fullscreen mode

Those artifacts increasingly cross application, vendor, and organizational boundaries.

They need many of the same properties APIs already have.

GTS is an attempt to provide a common type identity and relationship layer for that broader software ecosystem.

It is an open specification and open-source project:

https://github.com/GlobalTypeSystem/gts-spec

https://github.com/GlobalTypeSystem

I'm particularly interested in how people building event platforms, agent ecosystems, MCP infrastructure, schema registries, plugin systems, and extensible SaaS products deal with these questions today.

Are we dealing with separate problems?

Or are we repeatedly rebuilding fragments of the same missing type system?

Top comments (0)