§ 3 — The Standard
Exposers
3.0 Introduction
Exposers are disposable components in any system that expose your core business logic functionality by mapping its responses to a specific protocol. For instance, in RESTful communications, an API controller would be responsible for returning a 200 code for a successful response. The same applies to other protocols, such as gRPC, SOAP, or any other protocol of communication between distributed systems.
Exposer components are similar to Brokers. They are the last point of contact between the core business logic and the outside world. They are built with the intent that they will be detached from the current system at some point in time, allowing the same core logic to integrate with modern systems, protocols, or interfaces.
3.0.0 Purpose
In general, the primary responsibility of exposure components is to allow someone or something to interact with your business logic. For that core purpose, a precise mapping bit by bit to every possible response from your core business logic should be communicated cautiously with the consumer to that logic. I say cautiously because sometimes, specific internal issues in the system are not required to be exposed to the outside world. This mapping of responses can usually be a zero effort as the protocol and the language your code business logic communicates are the same as with libraries produced to run on the system that uses the same technologies or programming languages.
However, there are occasions where the outside world stateless protocol doesn’t necessarily match the exact value of a response. In this case, it becomes an exposer component’s responsibility to make a successful mapping both ways in and out of the system. API controllers are a great example of that. They will communicate a 4xx issue when there’s a validation exception of some type and return a deserialized JSON value if the communication is successful. However, there are also more details about problem details, error codes, and other levels of mapping and communication that we will discuss in upcoming chapters within this section.
3.0.0.0 Pure Mapping
The most important aspect of exposure components is that they are not allowed to communicate with brokers of any type. They are also not allowed to contain any form of business logic within them. By business logic here, I mean no sequence of routine calls, no iteration, or selection/decision-making. In the same way, it is with brokers only link an existing realm with the outside realm to achieve a specific value.
3.0.1 Types of Exposure Components
Exposure components are of three types: communication protocols, user interfaces, and IO routines. Let’s discuss those briefly.
3.0.1.0 Communication Protocols
An exposure component that is a communication protocol can vary from simple RESTful APIs to SOAP communication or gRPC. It can also be a simple client in a library, where consumers would just install the library in their projects and consume your core logic through the client APIs. These examples are all of the same type of exposure components.
The differentiator here is that a communication protocol is usually event-based. It is triggered by an incoming communication and treated with a response of any kind. Communication protocols are usually for system-to-system integrations, but they can be accessible and understandable by humans for testing and debugging purposes.
3.0.1.1 User Interfaces
Another type of exposure component is the user interface. This can vary from Web, mobile, or desktop applications to simple command lines. User interfaces mainly target end-users for communication but can be automated by other systems, especially with command-line interfaces. In this day and age, user interfaces can also include virtual and augmented realities, metaverses, and any other form of software.
There are occasions where Human-Machine-Interfaces (HMI) can also fall into that level of exposure components. For instance, the buttons on a cellphone, keyboards we use daily, and any form of hardware that can interact directly with core business logic interfaces as an exposure component. The same theory applies to the Internet of Things (IoT) components and many others where a human has to utilize a component to leverage a specific capability to their advantage.
3.0.1.2 I/O Components
Some exposure components are not necessarily systems interfacing with another system, and they are not purposely designed to communicate with humans. They are daemons or IO-based components that do something in the background without a trigger. Usually, these components are time-based, and they may leverage existing protocols or interface directly with the core business logic, both of which are viable options.
3.0.2 Single Point of Contact
Exposure components are only allowed to communicate with one and only one service. Integrating with multiple services would turn an exposure component into either orchestration or aggregation services, which are not allowed to exist as core logic in that realm of exposure.
The single point of contact rule also ensures the ease of disposability of the exposure component itself. It ensures the integration is simple and single-purposed enough with controlled dependencies (only one) that it can be rewired to virtually any protocol at any point with the least cost possible.
3.0.3 Examples
Let’s take API controllers as an example of a real-world exposure component in any system.
[HttpPost]
public async ValueTask<ActionResult<Student>> PostStudentAsync(Student student)
{
try
{
Student registeredStudent =
await this.studentService.RegisterStudentAsync(student);
return Created(registeredStudent);
}
catch (StudentValidationException studentValidationException)
when (studentValidationException.InnerException is AlreadyExistsStudentException)
{
return Conflict(studentValidationException.InnerException);
}
catch (StudentValidationException studentValidationException)
{
return BadRequest(studentValidationException.InnerException);
}
catch (StudentDependencyException studentDependencyException)
{
return InternalServerError(studentDependencyException);
}
catch (StudentServiceException studentServiceException)
{
return InternalServerError(studentServiceException);
}
}
The code snippet above is for an API method that POST a student model into the core business logic of a schooling system (OtripleS). In a technology like ASP.NET, controllers handle mapping incoming JSON requests into the Student model so that the controller can utilize that model with an integrated system.
However, you will also see that the controller code tries to map every possible categorical exception into its respective REST protocol. This is just a simple snippet to show what an exposure component may look like. But we will talk more about the rules and conditions for controllers in the next chapter of The Standard.
3.0.4 Versioning
3.0.4.0 Release Versioning
Release versioning governs how a system communicates what kind of change has been made at the release level. The Standard uses a strict four-segment format for all release versions:
v1.2.3.4
Each segment carries a fixed and non-negotiable meaning:
| Segment | Meaning |
|---|---|
| 1 | Model change |
| 2 | Service or routine change |
| 3 | Bug fix or configuration change |
| 4 | Automated build version |
When multiple change types occur in the same release, the highest-order change wins and all lower segments reset to zero. Given a current version of v1.2.3.4, the expected next versions for each change type are:
- model change →
v2.0.0.0 - service or routine change →
v1.3.0.0 - bug fix or configuration change →
v1.2.4.0 - automated build of the same code →
v1.2.3.5 - model + service change together →
v2.0.0.0
The version number must describe what kind of change happened, not merely that a change happened. That means a model change is never hidden inside the service segment, a service change is never hidden inside the bug or configuration segment, and a build increment is never used to represent a code change.
3.0.4.1 API Versioning
API versioning governs how routes communicate the model and behavior version to the consumers of a system. The initial API is exposed without any version prefix, keeping the route clean and straightforward:
api/Students
This is the V0 route. When a model change occurs, the route must surface the model version so that consumers understand they are interacting with a structurally different shape:
api/V1/Students
When the service behavior changes for an already-versioned model, the route surfaces both the model version and the behavior version together as a pairing:
api/V1.1/Students
In this shape, the number before the dot identifies the model version and the number after the dot identifies the behavior version. This pairing gives consumers a precise signal about both the shape of the data they will receive and the behavior they should expect from the service. If only the behavior changes but the model remains at version zero, the route becomes api/V0.1/Students — the model version is still zero, but the behavior version has incremented to one.
3.0.4.2 Deprecation
Deprecation signals that a version of an API or piece of code is no longer the recommended path forward and will eventually be removed. For deprecation to be useful, it must be visible. Consumers must be able to see that an API is deprecated, understand when it is expected to sunset, and know where to go for migration guidance. They must also be given enough time to act before the deprecated version disappears.
For APIs, a valid approach is to surface deprecation metadata through response headers using an attribute applied directly to the controller action:
[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
[HttpGet]
[DeprecatedApi(
Sunset = "2024-12-31",
Warning = "This API is deprecated. Please migrate to v2.",
Link = "https://example.com/deprecation-info")]
public IActionResult GetSampleData()
{
return Ok(new { message = "Sample data" });
}
}
The Attrify NuGet library provides the DeprecatedApi attribute and handles the emission of the appropriate deprecation headers automatically, making the lifecycle of an API a compile-time concern rather than a runtime configuration task.
For code that is no longer maintained or supported, the standard [Obsolete] attribute makes deprecation visible to maintainers and consumers at compile time:
[Obsolete("This version is deprecated and no longer maintained.")]
3.0.4.3 Capabilities
Capabilities are a discovery mechanism that allows consumers to understand what a system or provider supports before they ever attempt to invoke it. Without a capabilities surface, a consumer has no choice but to call an operation and react to whatever failure or exception comes back. With one, the consumer can check first and decide safely. This aligns directly with the broader Standard principle of avoiding invalid operations rather than reacting to them after the fact.
In a provider-based architecture, each concrete provider exposes a capabilities property that enumerates the operations it supports for each resource. The consumer queries this property before invoking any operation, making the interaction deterministic and safe:
IStudentProvider provider = new StudentProvider();
if (!provider.SupportsOperation("Student", "Add"))
{
return;
}
await provider.Students.AddAsync(new Student());
Capabilities may also be surfaced through Swagger, middleware-based metadata, bespoke capabilities endpoints, or provider-specific capabilities endpoints, depending on the architecture of the system. The goal in every case is the same: turn runtime uncertainty into a deterministic check that the consumer can perform before committing to a call.
3.0.5 Summary
In summary, exposure components are very thin layers that don’t contain any intelligence or logic. They are not meant to orchestrate or call multiple core business logic services. They only focus on the duplex mapping aspect of communication between one system and another.
This chapter lives on GitHub, where it is written in the open. Read the source or suggest a change.