paint-brush
A Table as an API? Illusions and Realityby@truewebber
496 reads
496 reads

A Table as an API? Illusions and Reality

by Aleksei Kish9mMarch 5th, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

The author argues that using a shared database table as a means for service-to-service communication is an anti-pattern. While it might appear to be a quick solution, it leads to versioning headaches, unclear ownership, and difficulties with scalability and security. Instead, the article advocates a “Contract First” approach, where each service formally defines its interfaces and retains ownership of its own data. This method fosters clearer accountability, smoother evolution, and more robust integration across teams.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - A Table as an API? Illusions and Reality
Aleksei Kish HackerNoon profile picture
0-item
1-item

Introduction

What is a “contract” in the context of service interaction?

In the context of interacting services modules, the inevitable question emerges: By what rules does communication take place? In IT products, a “contract” represents a formal understanding of what data flows between systems and how it is transmitted. This entails the data format (JSON, Protobuf, etc.), structural elements (fields, data types), communication protocol (REST, gRPC, message queues), and other specifications.


A contract ensures openness (everyone knows what is received and sent), predictability (we can update the contract and maintain versions), and reliability (our system will not fail if we make well-managed changes).

Why people tend to pick a table in the database as a “contract.”

In practice, although everyone talks about microservices, “contracts” and APIs, we often see people adopt the approach: “Why not create a shared table in the database instead of building APIs?”


  • Historical or organizational habit: When everything has always been stored in one DB system within one company, why reinvent the wheel?


  • The “quick fix” mentality: We’ll write, you’ll read without setting up authorization rules and designing API specifications.


  • The “big data” argument: When working with tens or even hundreds of gigabytes of data, direct transfer into a shared table appears simpler, faster, and more economical, but in practice, it creates scalability and performance issues as well as data ownership problems.


Therefore, while using a shared table for data exchange may seem efficient and optimized for quick results, it generates various technical and organizational challenges in the long run. However, when teams choose shared tables for data exchange, they may face numerous problems during implementation.

Why “a Table in the Database” Isn’t a Contract (and Why It’s an Anti-Pattern).

Lack of a Clearly Defined Interface

When services communicate via REST/gRPC/GraphQL, they have a formal definition: OpenAPI (Swagger), protobuf schemas, or GraphQL schemas. These define in detail which resources (endpoints) are available, which fields are expected, their types, and the request/response formats. When ‘a shared table’ acts as a contract there isn’t a formal description: There is no formal description of the contract; only the table schema (DDL) is available and even that is not well documented. Any minor modification of the table structure (e.g., adding or deleting a column, changing data types) can affect other teams that read from or write to this table.

Versioning and Evolution Problems

API versioning is a normal practice: We might have v1, v2, and so on, and we can keep backward compatibility and then gradually move clients to the newer versions. For database tables, we only have DDL operations (e.g., ALTER TABLE), which are tightly coupled to a specific DB engine and require careful handling of migrations.


There is no centralized system that can send alerts to consumers about schema changes that require them to update their queries. As a result, “under-the-table” deals may occur: Someone can post in a chat, “Tomorrow, we’re changing column X to Y”, but there’s no guarantee everyone will be ready in time.

No Clear Ownership

When there’s a clearly defined API, it’s evident who owns it: the service that serves as the API publisher. When multiple teams use the same database table there is confusion about who gets to determine the structure and which fields to store and how to interpret them. As a result, the table can become “no one’s property,” and every change becomes a quest: “We need to check with that other team in case they’re using the old column!”

Security and Access Control Issues

It’s hard to keep track of who can read and write to a table if many teams have access to the DB. There is a chance that unauthorized services can access the data even though it was not intended for them. It’s easier to manage such issues with an API: You can control the access rights (who can call which methods), use authentication and authorization, and monitor who called what. With a table, it’s much more complicated.

Dependency on Internal Structure

Any internal modifications to the data (reorganizing indexes, partitioning the table, changing the DB) become a global problem. If the table functions as a public interface, the owner cannot make internal changes without endangering all external readers and writers.

Pain Points and Typical Problems in Practice

Coordinating Changes

This is the most painful aspect: How does one go about informing another team that the schema will be changing the next day?

  • A successful scenario for updating the table version: The owner creates a new table with an updated schema in parallel with the old one. The old version remains accessible to current consumers and the owner sends them a message saying, “The new structure is available; check out the documentation and deadlines. Please migrate while both versions exist.”


  • However, in an OLAP scenario or with large data volumes, maintaining two parallel tables is not a trivial task. You also have to determine how to move data from the old to the new schema. This can sometimes require planned downtime or very sophisticated infrastructure. This process necessarily introduces both risk and extra work.

Data Integrity Problems

When multiple teams use a shared table to select and update critical data, it can easily become a “battlefield.” The result is that business logic ends up scattered across different services, and there is no centralized control of data integrity. It becomes very difficult to know why a particular field is stored in a particular way, who can update it, and what happens if it is left blank.

Debugging and Monitoring Challenges

For example, suppose the table breaks: Let’s say, there is bad data or someone has taken a lock on some crucial rows. Identifying the source of the problem can often require asking every team with DB access to determine what query caused the problem. It’s often not obvious: This means one team’s query might have locked up the database, while another team’s query produces the observable error.

Single-node failure drags everyone down.

A shared database is a single point of failure. If it goes down, then many services will go down with it. When the database has problems with performance because of one service’s heavy queries, all services experience problems. In a model with clear-cut APIs and data ownership, every team is masters of their service’s availability and performance, so a failure in one component does not propagate to others.

Providing a separate read-only replica does not solve the problem.

A common compromise is: “We’ll give you a read-only replica so you can query without affecting our main database.” At first, that might address some load problems, but:

  • Versioning issues remain. The main problem is, that when the main table structure changes, the replica’s structure changes too, just with some delay.


  • Replication lag can cause data states to be unpredictable, especially with large datasets.


  • Ownership is still unclear: Who defines the format, structure, and usage rules? A replica is still “a piece” of someone else’s database.

How to Properly Design Service Interaction (Contract First)

An explicit contract definition.

Modern design practices (for example, “API First” or “Contract First”) start with a formal interface definition. OpenAPI/Swagger, protobuf, or GraphQL schemas are used. This way, both people and machines know which endpoints are available, which fields are required, and what data types are used.

Service as Data Owner

In a microservices (or even modular) architecture, the assumption is that each service owns its data entirely. It defines the structure, storage, and business logic and provides an API for all external access to that API. Nobody can touch ‘someone else’s’ database: only official endpoints or events. This makes life easier whenever changes are in question and it is always clear who is to blame.

Implementation Examples

  • REST/HTTP: A service publishes endpoints like GET /items, POST /items, etc., and clients make requests with a well-defined data schema (DTO).


  • gRPC / binary protocols: In gRPC/protobuf, the service and messages are formally defined in .proto files, and changes are simply made to the .proto files where method, request, and response are defined.


  • Event-driven: The data-owning service publishes events to a broker such as Kafka or RabbitMQ, and subscribers consume them. The contract here is the event format. Structural changes are made through versioned topics or messages.

Version Control

No matter what model, it is both possible and essential to implement version control on the interface. For example:

  • In REST, we have /api/v1/… and /api/v2/.


  • With gRPC/protobuf, there are powerful mechanisms for backward/forward compatibility—new fields, messages, and methods can be added without breaking old clients while others marked as deprecated.


  • In event-driven architectures, you can publish old and new event formats in parallel until all consumers migrate.

Distributed Responsibility

A fundamental principle is that the team that owns the data gets to decide how to store and manage it, but they should not give direct write access to other services. Others must go through the API as opposed to editing foreign data. This yields clearer responsibility distribution: If service A is broken, then it is service A’s responsibility to fix it and not its neighbors.

Examples of Service Interaction

Within a Single Team

At first glance, if everything is in one team, why complicate things with an API? In reality, even if you have a single product split into modules, a shared table can lead to the same issues.


  • It’s better to create a “facade” or “microservice” that owns the `orders` table, for instance, and then other modules (like analytics) call this facade/service.


  • This keeps the contract principle explicit and simplifies debugging.


For example, the Orders service is the owner of the orders table, and the Billing service does not access that table directly – it makes calls to the Orders service’s endpoints to get order details or to mark an order as paid.

Between Two Teams

At a higher level, when two or more teams are responsible for different areas, the principles remain the same. For instance:

  • Team A is responsible for the product catalog service that contains information about each item (price, availability, attributes).


  • Team B takes care of the shopping cart service.


If Team B directly queries the “Catalog” table belonging to Team A, any internal schema changes at A (e.g., adding fields, altering structure) may affect Team B.


The proper approach is to use an API: Team A provides endpoints like GET /catalog/items, GET /catalog/items/{id}, etc., and Team B uses those methods. If A is able to support older and newer versions, they can release /v2, which gives B time to migrate.

Organizational Aspects and Benefits

Transparent Communication

With a formal contract, all changes are visible: in Swagger/OpenAPI, .proto files, or event documentation. Any update can be discussed beforehand, properly tested, and scheduled, with backward compatibility strategies as needed.

Faster Development

Changes in one service have less impact on others. The team does not have to worry about “breaking” someone else if they properly manage new and old fields or endpoints, ensuring a smooth transition.

Access and Security Management

API gateways, authentication, and authorization (JWT, OAuth) are standard for services, but nearly impossible with a shared table. It’s easier to fine-tune access (who can call which methods), keep logs, track usage statistics, and impose quotas. This makes the system safer and more predictable.

Conclusion

A shared table in the database is an implementation detail rather than an agreement between services thus not considered a contract. The many issues (complex versioning, chaotic changes, unclear ownership, security, and performance risks) make this approach untenable in the long run.


The correct approach is Contract First which means defining interaction through formal design and following the principle that each service remains the owner of its data. This not only helps to decrease technical debt but also increases transparency, speeds up product development, and enables safe changes without having to engage in firefighting over database schemas.


It is both a technical issue (how to design and integrate) and an organizational issue (how teams communicate and manage changes). If you want your product to grow without having to deal with endless emergencies regarding database schemas, then you should start thinking in terms of contracts rather than direct database access.