Microservices: Information Hiding
The designer must provide the intended user with all the necessary information to use the module correctly, and nothing more.
Information hiding is a principle of software architecture introduced by David Parnas in the classic paper “On the Criteria to Be Used in Decomposing Systems into Modules”. This concept, which revolves around modularity, has gained significant relevance in recent years in the context of microservices, where deployment independence and reduced coupling are essential. By hiding a service’s internal details and exposing only what is necessary through well-defined interfaces, it becomes possible to build systems that are more resilient, flexible, and easier to evolve.
In this article, we will explore how information hiding can be applied to microservices, based on three key sources:
• David Parnas’s paper, which introduced the concept in the context of modularity.
• Sam Newman’s perspective in his book Building Microservices and his interview with InfoQ, where he connects information hiding to independent deployability.
• Practical examples that highlight the benefits, common mistakes, and tools that can assist us.
Let’s dive into this important topic!
Information Hiding: What Does It Really Mean?
I love analogies, so here’s one to clarify the concept. Imagine that each microservice is like a house, and its public interface is a mailbox. The house (the microservice) can undergo internal renovations—moving walls, redesigning rooms, fixing the roof, replacing furniture, or even redoing the entire electrical system. However, the mailbox remains in the same place, accessible and functional for the mailman and any other correspondence that arrives.
The mailman (representing the consumers of your service) doesn’t need to know what’s happening inside the house. He only needs access to the mailbox—clear, well-positioned, and stable. As long as the mailbox continues to function as usual, the house can undergo modifications freely without affecting the mailman’s work.
Now, imagine the chaos if, with every renovation, you moved the mailbox or changed its design without informing the mailman. He wouldn’t be able to deliver the mail, and the whole system would fail.
Information hiding follows this exact principle: keep only the essential elements (like the mailbox, or public interfaces) stable and accessible, while allowing the rest to change freely without impacting those consuming the service.
The main idea is simple but powerful: what happens inside the module (or microservice) should stay inside it. What is shared externally should be only the minimum necessary for other modules to interact safely. It may seem obvious, but in practice, this approach can completely transform how systems are built and maintained.
Why Is This So Important?
Parnas explored this concept decades ago and identified three major benefits that remain entirely relevant today:
1. Better Development Timeline
Have you ever wondered why projects with multiple teams can drag on for months longer than expected? It’s often because teams become entangled in dependencies between modules. When one module is modified, others break, leading to rework.
Information hiding enables modules to be developed independently. With each team focusing on its own domain and relying only on shared interfaces (like the mailbox in the example), the work becomes more parallel, reducing bottlenecks. In other words, more hands can work on the system simultaneously with fewer conflicts.
2. Improved Comprehension
Imagine opening the code for a module and realizing it directly depends on the internal details of five other modules. The confusion this creates makes the code harder to read, understand, and, consequently, evolve.
When we apply information hiding, each module can be understood in isolation. The overall complexity of the system is reduced because you only need to deal with what’s within the boundaries of that module. Just like in the analogy, you don’t need to know how the house was renovated; you only need to know that the mailbox is there and working.
This not only makes developers’ jobs easier but also simplifies onboarding for new team members.
3. Flexibility
Here’s the most crucial point: a module that effectively hides its information can be modified without requiring other modules to know about or adapt to these changes. Want to redesign the database or refactor some internal logic? Go ahead! If no one is “peeking” into your module, you have complete freedom to evolve it however you like.
Moreover, this flexibility enables innovative combinations. You can reuse or recombine modules in creative ways, building new features with minimal effort—without disrupting the work of other teams.
These three benefits—better timelines, improved comprehension, and flexibility—are more than just technical advantages. They form the foundation for development teams to deliver sustainable and scalable systems, even under tight deadlines and ever-changing requirements.
But How Does This Relate to Microservices?
Now you might be wondering, “Okay, I get the importance of this. But what does it have to do with microservices and modularity?”
One of the biggest appeals of microservices architecture is the promise of independent deployability. After all, who wouldn’t want the ability to update or redeploy a specific service without impacting the rest of the system?
However, as Sam Newman highlights in his interview on the InfoQ podcast, achieving this independence requires more than good intentions—it demands adopting information hiding as a core principle.
The Definition of Information Hiding in Microservices
In the interview, Newman provides a clear definition: “If you think about a microservice boundary, your default position is almost that you expose nothing.”
This principle—exposing only the bare minimum through external interfaces—is at the heart of information hiding.
When you expose methods, data structures, or functionalities of a service, you create an implicit or explicit contract with external consumers. Once this contract is established, any changes to the microservice must consider the impact on the consumers that depend on this information.
The result? The microservice loses its freedom to evolve and, consequently, its ability to be independently deployed.
The Connection Between Information Hiding and Independent Deployability
Information hiding is directly tied to a microservice’s ability to be modified or redeployed without “breaking” external consumers. Newman emphasizes that “the more you hide, the more freedom you have as a developer working on a microservice to safely change things.”
This means the key to achieving independence lies in concealing unnecessary details and limiting what is shared to the essentials.
When a microservice’s boundaries are well-defined and its internal implementation is fully isolated, changes made within those boundaries won’t cause a ripple effect across the system. On the other hand, when internal details leak—intentionally or accidentally—even minor changes can snowball into complex problems for consumers.
Hiding as a Form of Clarity and Control
For Newman, information hiding isn’t just a technical concern; it’s about clarity. He says: “Hiding information is really about being clear to a developer about which things can be safely changed and which cannot.” This clear understanding creates an environment where developers can make changes without the constant fear of breaking integrations or causing production issues.
Moreover, information hiding emphasizes the importance of explicit interface contracts. Newman highlights that well-defined contracts are a powerful tool to ensure changes can be made safely. For example, by using REST contracts, gRPC, or events, developers know exactly what can be modified and what requires backward compatibility.
The Relationship Between Contracts and Information Hiding
Contracts are the point of interaction between a microservice and its consumers. They represent the explicit “agreement” about what the service provides and how consumers should use it. In this context, information hiding plays a critical role: it ensures that the contract exposes only what is necessary, while internal details remain encapsulated within the microservice.
As Sam Newman explains in his interview, “anything you expose becomes part of the external contract.” This means that if a microservice exposes unnecessary fields or endpoints, those details become liabilities—even if you didn’t intend them to be. Changing them in the future could break consumers, undermining the microservice’s deployability independence.
By hiding information, the microservice keeps its contracts small, simple, and reliable. This enables:
• Stability: Internal changes within the microservice do not affect consumers because the contract remains unchanged.
• Evolution: Services can be reimplemented or refactored without causing disruptions to the system.
Therefore, information hiding and clear, explicit contracts work together to ensure that microservices are isolated, predictable, and sustainable.
What to Hide and What to Expose?
Newman suggests a practical model for dealing with information hiding:
• What should be hidden: Any internal detail of business logic, data structures, or implementation decisions that are not essential for external consumers.
• What should be exposed: Only what is necessary for consumers to interact with the service. This typically includes REST endpoints, gRPC calls, or event messages.
For instance, imagine a microservice responsible for managing orders in an e-commerce system. Internally, it might use complex structures to calculate shipping costs or apply discounts. These details do not need to be exposed. Instead, what should be visible is only the result of those operations: the final order price, made available through an API.
Impacts of Information Hiding in Everyday Work
For developers, information hiding is an operational relief. As Newman puts it:
“Developers don’t go to work every day wanting to break systems; they want to do a good job.”
When the boundaries of a microservice are well-defined and what is exposed is clear and limited, developers can focus on implementing and continuously improving the service without the fear of causing a cascade effect across the system.
Furthermore, this significantly reduces the cost of maintaining and evolving the system. Since the contracts are stable and well-defined, integrations between services become easier to manage, and services can be redeployed independently.
Hiding Is the Foundation!
When we talk about independent deployability in microservices, it’s easy to focus solely on the act of deploying a service to production. But, as Sam Newman points out, deployability is much more than that. It’s about the ability of a service to evolve, be updated, or replaced without disrupting the rest of the system. It’s about giving teams the freedom to work in isolation, confidently, without fear of breaking other services.
Now, think about what makes this possible. In an ideal world, each microservice is like an independent unit that can be changed and deployed without impacting consumers. But in the real world, where distributed systems are inherently complex, this independence only exists if there are well-defined boundaries and clear contracts. Most importantly, it depends on keeping the internal details of each microservice hidden—and this is the fundamental role of information hiding.
As David Parnas aptly stated, the goal of design is to provide the user with “all the information necessary to use the module correctly, and nothing more.” In microservices, this principle translates to exposing only what is indispensable through APIs or events, while keeping everything else hidden. It’s this separation that allows internal changes to be made without the fear of breaking consumers.
Then, there’s the impact on integrations. Imagine a microservice needing an update. If it exposes more than it should—internal details or unnecessary logic—any change can cause a cascade effect on consumers. This isn’t independent deployability; it’s creating a fragile system. By hiding what’s irrelevant to consumers, the microservice becomes flexible. It can evolve, improve, or even be replaced while the contract with its consumers remains intact.
And of course, there’s the production environment. Deployability is also about resilience. Services that respect clear boundaries and hide their internal details are easier to monitor and fix when issues arise. Imagine a service that excessively exposes its details. Any internal problem could directly affect consumers, making recovery more complex. Conversely, when details are encapsulated, failures can be contained and resolved without causing major disruptions to the rest of the system.
The Relationship Between Information Hiding and Modularity in Microservices
Now that we understand the benefits of information hiding, let’s take it a step further and discuss how this concept is directly connected to modularity. After all, microservices are essentially an attempt to apply modularity principles in the world of distributed systems.
If you think of microservices as just a modern way to structure systems, Sam Newman has a different perspective: they are, above all, a modular architecture. But, as he points out, they come with the added complexity of a distributed system. This is where information hiding becomes an indispensable pillar.
Modularity and Hiding: Two Sides of the Same Coin
Modularity, as explored by David Parnas, is a way of breaking a system into smaller parts, called modules, with well-defined boundaries. Each module should hide its internal information, exposing only what is essential for interaction with other modules. This separation brings three key advantages: independence, simplicity, and flexibility—all fundamental to microservices.
Now think about microservices: aren’t they essentially modules? However, instead of being parts of a single monolithic system, they are independent services that interact with one another. The difference lies in how these interactions occur: in a distributed system, communication happens via APIs, messages, or events rather than directly within the code.
Newman also emphasizes that information hiding reduces the “weight” of contracts between services. The fewer details a service exposes, the fewer expectations it creates for its consumers. This enables internal changes to be made with greater freedom, without the constant fear of breaking other services.
“Any method, structure, or functionality you expose becomes part of your external contract. Once consumers depend on it, it becomes difficult to change.”
Here’s an interesting point: even decades before the era of microservices, David Parnas was discussing concepts that resonate to this day and remain highly relevant. Let’s see how his ideas translate into the world of microservices:
1. Well-Defined Boundaries: Parnas argued that each module should have clear boundaries. In microservices, this is reflected in well-designed APIs and the concept of bounded contexts in domain-driven design.
2. Implementation Hiding: For Parnas, hiding internal details was essential. In microservices, this means limiting what each service exposes, whether through REST APIs, gRPC, or events.
3. Reduction of Assumptions: Parnas stated that a module should make as few assumptions as possible about other modules. In microservices, this translates to clear and minimal contracts between services, allowing for greater independence.
If Parnas were to observe today’s microservices, he would likely see them as the natural evolution of his vision of modularity. However, as Sam Newman points out, working with modules in a distributed system is far more complex than in a monolith. The interactions between microservices are inherently more intricate, requiring extra care in defining boundaries and contracts.
Therefore, modularity in microservices is enabled by information hiding, which ensures that each service maintains its isolation and functions as an autonomous unit. Without this, modularity is lost, and microservices become nothing more than fragmented parts of a distributed monolith.
And, as we’ve seen, both Parnas and Newman would agree that without this, any promise of flexibility and scalability in microservices falls apart. Let’s dive into this in the next section. 👇🏼
How Can a Microservice Violate Information Hiding?
Have you ever encountered a microservice that seems to work well, but suddenly a small change causes chaos throughout the system? Maybe a field that was “improvised” in an API response broke a consumer, or an internal rule was exposed, and now everyone depends on it. This is the kind of problem that arises when the principle of information hiding is not respected. But how does this happen, exactly?
Picture a microservice managing orders in an online store. Its job is to process an order’s status and inform the consumer whether the order has been delivered, is in transit, or is awaiting processing. So far, so good. The problem starts when the service begins exposing more information than it should.
Instead of returning something simple like:
{
"status": "Delivered"
}
It decides to include some extra fields, such as:
{
"status": "Delivered",
"internalOrderId": "abc123",
"processingStep": "validation_completed",
"deliveryCode": "D001"
}
Perhaps the initial intent was to make the work easier for another service consuming these data, but in practice, this creates a trap. Consumers start using fields like processingStep
or deliveryCode
, which were originally intended for internal use only.
Now, if the team responsible for the microservice decides to refactor the logic or rename processingStep
to something more appropriate, like currentStage
, multiple consuming services will break. Suddenly, a change that should have been internal turns into a massive problem.
How Can a Microservice Expose Internal Business Rules?
Imagine this scenario: you have a service responsible for managing orders in an e-commerce system, and it has an internal rule that states:
“If an order status remains ‘Processing’ for more than 24 hours, the order will be automatically canceled.”
This is a completely internal rule that should be applied and managed solely by the order microservice itself. However, at some point, this rule might leak and be utilized by external consumers in a few ways:
The Rule Is in the API Response
The order microservice exposes an endpoint, for example, /orders/{id}
, that returns the order status. However, instead of keeping internal fields (which are part of the rules) hidden, it includes unnecessary information in the response, such as:
{
"orderId": "12345",
"status": "Processing",
"timeInStatus": "25 hours"
}
It seems like a harmless payload… But the timeInStatus
field shouldn’t be there. It’s an internal detail used by the microservice to monitor how long an order has been in a particular state. However, by exposing this in the API, the consumer might start inferring the business rule and using it in their own code, creating messages like:
“Your order has been in processing for more than 24 hours and will be canceled soon.”
The problem here is that the consumer has now hardcoded a direct dependency on this internal rule. If the order microservice changes the time limit from 24 hours to 48 hours, or decides not to automatically cancel the order, the consumer will continue acting as if the old rule were still valid.
The Rule Is Encoded in an Event
Another common scenario happens in event-driven systems. Imagine the order microservice publishes an event to other services whenever an order’s status changes. The event might contain something like:
{
"orderId": "12345",
"status": "Processing",
"timeInStatus": "25 hours"
}
Here, once again, the timeInStatus
field is exposed, allowing external consumers to infer that after 24 hours in Processing, the order will be canceled. For example, the notification microservice might use this field to create its own logic for sending messages to the customer, such as:
“Your order will be automatically canceled soon.”
If the order microservice changes its logic to no longer automatically cancel orders or adjusts the time limit, the original event will not be sufficient to communicate this change. The consumer will continue acting based on the old and entirely incorrect logic.
Why Is This a Problem?
When external consumers start using exposed business rules—whether through the API, events, or informal communication—they create an undesirable coupling with the service’s internal logic. The microservice loses its freedom to evolve, as any rule change risks breaking the consumers. In the example above:
• If the time limit changes from 24 to 48 hours, the notification service will still send incorrect messages.
• If the order microservice decides to reprocess orders instead of canceling them, consumers won’t know about this change and will continue acting based on the outdated logic.
Got it? The problem is clear. Now, let’s analyze the solution.
How to Avoid This?
The key to avoiding these problems is to hide business rules. The order microservice, for instance, could:
• Avoid exposing details like timeInStatus or any other field that allows consumers to infer internal logic.
• Publish only high-level events, such as OrderCancelled, instead of including details about the cancellation process.
• Encapsulate the rule and return only the final result. For example:
Instead of allowing the consumer to infer when an order will be canceled, the microservice could include an explicit field like:
{
"orderId": "12345",
"status": "Processing",
"actionRequired": "None"
}
By doing this, the consumer doesn’t need to worry about the internal logic or how the time in Processing is handled. The microservice encapsulates this logic, maintains control, and can evolve without fear of breaking other services.
What’s the Point I’m Trying to Make?
How do these problems happen in practice? Often, it’s a matter of haste. A developer might be under pressure to deliver quickly and think, “I’ll just add this field to the response to solve the consumer’s problem for now.” Or perhaps the team is eager to seem “helpful” and decides to expose more data than necessary, thinking it will benefit other teams.
The issue is that once you expose a field or a piece of logic, it becomes part of the service’s contract, and trying to remove it later is like “putting the genie back in the bottle.” It’s complicated.
The key to avoiding these situations is simple yet powerful: expose only what is essential.
• If the consumer needs to know the order status, give them only the status—not how you arrived at that conclusion or the internal details of your system.
• If you have a business rule, encapsulate the logic and return only the result, such as “Action required: Reprocess Order.” Don’t expose the why or the how.
Architectural boundaries must encapsulate decisions. This encapsulation reduces the ripple effect of changes, protecting the system from unintended side effects. – Mark Richards & Neal Ford, Fundamentals of Software Architecture
Information hiding is about designing systems that can change and evolve without causing unnecessary impacts on those consuming their services. After all, no one wants to receive frantic calls or messages because a small internal change triggered a ripple effect across the entire system.
Tools: API Gateway and AsyncAPI
Now that we’ve covered the importance of clear contracts, let’s take a look at how tools can help build and manage these contracts efficiently. While I won’t mention OpenAPI, as it’s also a valid option for creating contracts, these tools not only organize communication between services but also help protect internal information and keep interfaces simple and clear. Shall we explore how they do this?
API Gateway
Think of an API Gateway as the “front door” to your microservices. It acts as an intermediary, ensuring that consumers (other services or external clients) only see what’s necessary, completely hiding the internal details of the service.
How does the API Gateway help?
1. Abstraction Layer: It provides a single access point for your microservices’ APIs, allowing you to control what is exposed and what remains hidden. For example:
• If your microservice has multiple internal endpoints: The Gateway can consolidate them and expose only one external endpoint representing the desired functionality.
2. Information Hiding: Even if the microservice undergoes internal changes, such as modifications in logic or endpoint names, the consumer doesn’t need to know. The Gateway translates external requests to the internal services, keeping the public contract stable.
3. Clear Contracts: It allows you to define explicit and well-documented contracts for consumers, ensuring that unnecessary or internal details are not accidentally exposed.
Practical Example:
Imagine you have a microservice that manages orders with three internal endpoints:
• /orders/create
• /orders/update
• /orders/internal-status
An API Gateway can consolidate these into a single public endpoint:
• /orders
(used for creating and managing orders).
The consumer will never know about the existence of /orders/internal-status because the Gateway completely hides that part.
AsyncAPI: Contracts for Event-Driven Systems
If an API Gateway is an excellent solution for REST and synchronous APIs, AsyncAPI is the equivalent for message- and event-driven systems. It helps define and document clear contracts for services that communicate asynchronously, such as those using Kafka, RabbitMQ, or MQTT.
How does AsyncAPI help?
1. Standardized Documentation: It allows you to describe message topics, formats, and communication flows, ensuring all services know exactly what to send or receive.
2. Information Hiding: Similar to the API Gateway, AsyncAPI helps limit what is exposed in events. For example:
• A service might publish an event like OrderCreated with fields such as orderId and customerName. Internal details, such as internalStatus or auditLog, never need to appear in the message.
3. Explicit Contracts: With AsyncAPI, you can formalize the contracts between event producers and consumers, reducing the risk of misunderstandings or unintended dependencies.
Practical Example:
Imagine the “Orders” service publishes an event whenever a new order is created:
{
"event": "OrderCreated",
"data": {
"orderId": "12345",
"customerName": "John Doe"
}
}
With AsyncAPI, you can document exactly how this event works, ensuring that consumers know the format and required fields. Any internal details, such as logs or intermediate steps, remain encapsulated.
API Gateway and AsyncAPI in Action: Working Together
Both tools are essential in modern architectures. While the API Gateway manages synchronous communication (REST/gRPC) and hides internal endpoint details, AsyncAPI does the same for event-driven systems.
In the end, their goal is the same: to maintain clear contracts, hide unnecessary information, and ensure your services are resilient and easy to evolve.
A Summary of This Article for You!
We’ve reached the end, and now it’s time to reflect on everything we’ve discussed.
Remember David Parnas’s concept of modularity? He taught us that a well-designed system is one where each module (or, in our case, each microservice) hides its internal decisions, exposing only what is strictly necessary. This simple act of hiding information has a profound impact on how we design, develop, and maintain systems.
Development Timeline
How often have you seen projects delayed because teams were stuck in endless dependencies? Information Hiding solves this by allowing each team to work on its microservice independently, relying only on well-defined contracts. It’s like an organized factory: each group has its role, and no one needs to know how others operate internally. The result? Fewer bottlenecks, fewer conflicts, and faster delivery.
Comprehension
Imagine opening a microservice’s code and finding it depends on the internal details of five other services. This not only makes maintenance harder but also renders the entire system difficult to understand. When we apply Information Hiding, each microservice becomes a well-defined “black box” that is easy to understand on its own. Just like knowing where a house’s mailbox is enough to send a letter, in a microservice, you only need to know what the public API promises — the rest is irrelevant.
Flexibility
This is perhaps the greatest advantage. When you hide information and keep contracts stable, you can change everything behind the scenes: refactor logic, switch databases, or even rewrite the entire microservice. No one outside the service needs to adapt. This is true decoupling — the freedom to innovate without causing a domino effect across the system.
Hiding Information Is Decoupling Microservices
In the end, it all comes down to a powerful idea: hiding information is crucial to decoupling microservices. And decoupled microservices are easier to change, quicker to evolve, and more resilient to change. This separation is what enables each service to be independent, both in its logic and its deployment.
So, here’s the challenge: look at your microservices. Are they really hiding what they should? Are your contracts clear and limited? If the answer is “no” to either of these questions, it might be time to re-evaluate.
Thank you for reading to the end! That’s it for now! See you in the next post! 😄