The concern that is everywhere and nowhere
A line-of-business REST service usually has a clear job: read a customer, store an order, search an article. That is the business logic. But before any of it may run, the same questions have to be answered on every single request:
None of these concerns belong to "read a customer". They apply to all endpoints, they cut across the whole service, and in software design they are called cross-cutting concerns. Authentication, logging, caching, validation, auditing, rate limiting and transaction handling are the classic examples. The defining property: the concern is orthogonal to what the endpoint actually computes, yet the endpoint cannot run correctly without it.
Why mixing them in creates noise
The naive way is to write the concern directly into each handler method. The result looks like this:
The one line that carries business value — reading the customer — is buried under roughly twenty lines of infrastructure. Now multiply the problem. A service with five endpoints and four cross-cutting concerns does not need four implementations; it needs twenty, because every concern is copy-pasted into every method.
That copy-paste is where quality erodes:
The concern is not wrong — it is just in the wrong place. It should exist once, not once per endpoint. Of course, this problem is not special in microservice/RESTful coding — it applies to all of your desktop application interfaces as well — but for microservices we have a solution.
The RestInterceptor concept
Xbase++ V3 provides the base class RestInterceptor for exactly this separation. An interceptor is a small class that runs around a handler method without the method knowing it exists. The handler keeps only business logic; the concern lives in the interceptor.
Every interceptor derives from RestInterceptor and may override three hooks:
The interesting part is :before(), because it decides whether the method runs at all. It does that by voting. Before it returns, :before() must call exactly one of three vote methods:
Voting is mandatory: an interceptor whose :before() returns without a vote is a contract violation and is treated as an error. That rule sounds strict, but it is what makes the chain predictable — every interceptor either lets the request through, stops it, or answers it.
Three interceptors, three outcomes
Each of the three votes maps to one everyday concern. The code below is deliberately minimal.
Logging — :voteCommit(). Record the call, then let it proceed:
Authentication — :voteAbort(). Reject the request before the method ever runs:
Caching — :voteIgnore(). Answer straight from the cache on a hit, and fill the cache on a miss:
Notice what is not in these classes: no customer logic, no database access, no knowledge of what "getById" does. And notice what is no longer in the handler: no logging, no token check, no cache plumbing. The handler method is back to its one line of business logic.
Registration, filtering and order
Interceptors are attached to a handler in its
nRegister() callback — a class method that also registers the routes and parameter types. The order of registration is the order of execution, so the chain is explicit and readable:
:addInterceptor() takes two arguments: the interceptor class name as a string, and the method filter. The framework instantiates the class and applies the filter for you. The filter decides which handler methods the interceptor sees:
Caching only makes sense for reads, so the cache interceptor is filtered to the three GET methods; it is never consulted for a POST, PUT or DELETE. Interceptors can also be switched on and off at runtime with :enable() and :disable() without removing them from the chain — useful for enabling verbose logging in a specific deployment.
Because RestInterceptor builds on the framework's resilience layer, each interceptor also participates in the service's health monitoring. A concern that misbehaves is observed by the same mechanism that supervises the rest of the microservice, rather than failing silently.
What you actually gain
The benefit is easiest to see as a ratio. With N endpoints and M cross-cutting concerns:
For the five-endpoint, four-concern service that meant twenty inline blocks; with interceptors it is four small classes. Adding a sixth endpoint costs one business method and zero concern code — authentication, logging and caching apply to it automatically the moment it is registered.
Concretely, for a LOB developer this means:
This is not a new idea in the industry — it is the same separation that aspect-oriented frameworks and servlet filters provide in the Java world. What matters here is that Xbase++ V3 offers it as a first-class, typed class inside the microservice framework, so LOB code stays in Xbase++ and stays focused on the business.
A production-ready example
The snippets above are teaching-sized. A complete, runnable service that uses exactly this pattern is available as a public example for Xbase++ V3:
https://github.com/alaska-software/advanced-customer-endpoint
It is a REST microservice for customer data with full CRUD operations over a DBF/CDX database, and it ships three real interceptors registered in that order: LoggingInterceptor (request/response logging with timing), AuthInterceptor (JWT validation on protected endpoints) and CacheInterceptor (result caching for the read methods getAll, getById and getByName). Authentication is issued through a separate /auth/login endpoint that returns a bearer token; the auth interceptor then validates that token on the customer routes.
The example also shows how interceptors sit next to the other production concerns of a microservice — a stateless workarea container (WAContainer) for multi-threaded database access, a recovery manager, and CPU/memory health watchdogs — and it can run both as a console application and as a Windows service. The repository holds the current routes, the data schema and the build and deployment steps. It is the recommended starting point for turning the pattern shown here into a service of your own.
One caveat the example states plainly: its JWT signing secret is hard-coded for demonstration. A real deployment must load the secret from configuration or the environment, use password hashing and run over TLS. The interceptor pattern is production-shaped; the demo credentials are not.
Summary
Cross-cutting concerns — authentication, logging, caching, error handling — are necessary but do not belong inside business methods. Written inline they multiply across endpoints, drift out of sync and bury the logic that matters. The Xbase++ V3 RestInterceptor class isolates each concern in one place and runs it around the handler through three hooks — :before(), :after() and :nError() — with a three-way vote (commit, abort, ignore) that decides whether the method runs, is stopped, or is answered from elsewhere. The handler goes back to doing one job. The concern is written once and applied by registration and filtering. For LOB teams the result is less code, more consistency, cheaper change and fewer defects — the same engineering payoff that filters and aspects deliver in other stacks, available directly in Xbase++.
A line-of-business REST service usually has a clear job: read a customer, store an order, search an article. That is the business logic. But before any of it may run, the same questions have to be answered on every single request:
- Is the caller authenticated and allowed to do this?
- Should the request be written to the log, with timing?
- Has this exact read been answered before, so it can be served from a cache?
- What happens if the method throws an error — how is the error turned into an HTTP response?
None of these concerns belong to "read a customer". They apply to all endpoints, they cut across the whole service, and in software design they are called cross-cutting concerns. Authentication, logging, caching, validation, auditing, rate limiting and transaction handling are the classic examples. The defining property: the concern is orthogonal to what the endpoint actually computes, yet the endpoint cannot run correctly without it.
Why mixing them in creates noise
The naive way is to write the concern directly into each handler method. The result looks like this:
Code:
METHOD CustomerHandler:getById( cId )
LOCAL oCustomer, nStart := Seconds()
// --- auth ---
IF ! ::isAuthenticated()
::setError( 401, "Unauthorized" )
RETURN NIL
ENDIF
// --- cache lookup ---
IF ::cache:has( "getById", cId )
RETURN ::cache:get( "getById", cId )
ENDIF
// --- logging ---
XppFileLogger():info( "getById " + cId )
// --- the actual work: one line ---
oCustomer := CustomerDataMgr():getById( cId )
// --- cache store + logging again ---
::cache:put( "getById", cId, oCustomer )
XppFileLogger():info( "getById done " + Str( Seconds() - nStart ) )
RETURN oCustomer
The one line that carries business value — reading the customer — is buried under roughly twenty lines of infrastructure. Now multiply the problem. A service with five endpoints and four cross-cutting concerns does not need four implementations; it needs twenty, because every concern is copy-pasted into every method.
That copy-paste is where quality erodes:
- Drift. One endpoint logs the duration, another forgets. One checks the token, a newly added one does not - and now you have a security hole nobody wrote on purpose.
- Change cost. Switching the log format, or the cache-key strategy, means editing every method instead of one place.
- Unreadable handlers. The business intent disappears in boilerplate, which makes review and maintenance slower and more error-prone.
- Hard to test. You cannot test "read a customer" without dragging authentication and caching into the test.
The concern is not wrong — it is just in the wrong place. It should exist once, not once per endpoint. Of course, this problem is not special in microservice/RESTful coding — it applies to all of your desktop application interfaces as well — but for microservices we have a solution.
The RestInterceptor concept
Xbase++ V3 provides the base class RestInterceptor for exactly this separation. An interceptor is a small class that runs around a handler method without the method knowing it exists. The handler keeps only business logic; the concern lives in the interceptor.
Every interceptor derives from RestInterceptor and may override three hooks:
- :before() — runs before the handler method. This is where you authenticate, start a timer or look into a cache.
- :after() — runs after a successful method call. It receives the result and may transform it or record it.
- :onError() — runs if the method raised an error. It returns .T. when it has handled the error, or .F. to let the default error handling proceed.
The interesting part is :before(), because it decides whether the method runs at all. It does that by voting. Before it returns, :before() must call exactly one of three vote methods:
- :voteCommit() — let the handler method execute normally.
- :voteAbort() — do not execute the method; use the error the interceptor set beforehand (e.g. 401). Typical for a failed auth check.
- :voteIgnore( xResult ) — do not execute the method; return xResult instead. This is how a cache short-circuits a read.
Voting is mandatory: an interceptor whose :before() returns without a vote is a contract violation and is treated as an error. That rule sounds strict, but it is what makes the chain predictable — every interceptor either lets the request through, stops it, or answers it.
Three interceptors, three outcomes
Each of the three votes maps to one everyday concern. The code below is deliberately minimal.
Logging — :voteCommit(). Record the call, then let it proceed:
Code:
CLASS LoggingInterceptor FROM RestInterceptor
EXPORTED:
METHOD before()
METHOD after()
ENDCLASS
METHOD LoggingInterceptor:before( oHandler, cMethod, aParams )
XppFileLogger():info( "-> " + cMethod )
::voteCommit() // proceed
RETURN SELF
METHOD LoggingInterceptor:after( oHandler, cMethod, xResult )
XppFileLogger():info( "<- " + cMethod )
RETURN xResult // result unchanged
Authentication — :voteAbort(). Reject the request before the method ever runs:
Code:
CLASS AuthInterceptor FROM RestInterceptor
EXPORTED:
METHOD before()
ENDCLASS
METHOD AuthInterceptor:before( oHandler, cMethod, aParams )
IF ::validToken( oHandler )
::voteCommit() // token ok -> proceed
ELSE
oHandler:setError( 401, "Unauthorized" )
::voteAbort() // stop; the method never runs
ENDIF
RETURN SELF
Caching — :voteIgnore(). Answer straight from the cache on a hit, and fill the cache on a miss:
Code:
CLASS CacheInterceptor FROM RestInterceptor
EXPORTED:
METHOD before()
METHOD after()
ENDCLASS
METHOD CacheInterceptor:before( oHandler, cMethod, aParams )
LOCAL xHit := ::lookup( cMethod, aParams )
IF xHit != NIL
::voteIgnore( xHit ) // serve cached result, skip method
ELSE
::voteCommit() // miss -> let the method run
ENDIF
RETURN SELF
METHOD CacheInterceptor:after( oHandler, cMethod, xResult )
::store( cMethod, xResult ) // remember for next time
RETURN xResult
Notice what is not in these classes: no customer logic, no database access, no knowledge of what "getById" does. And notice what is no longer in the handler: no logging, no token check, no cache plumbing. The handler method is back to its one line of business logic.
Registration, filtering and order
Interceptors are attached to a handler in its
Code:
CLASS METHOD CustomerHandler:onRegister( oEndpoint )
// ... parameter types and route mappings via :addType() / :map() ...
// Register interceptors in order of execution:
::addInterceptor( "LoggingInterceptor", NIL ) // 1. all methods
::addInterceptor( "AuthInterceptor", NIL ) // 2. all methods
::addInterceptor( "CacheInterceptor", { "getAll", "getById", "getByName" } ) // 3. reads only
RETURN
:addInterceptor() takes two arguments: the interceptor class name as a string, and the method filter. The framework instantiates the class and applies the filter for you. The filter decides which handler methods the interceptor sees:
- NIL — apply to all methods.
- a method name — apply to that one method.
- an array of names — apply to those methods.
Caching only makes sense for reads, so the cache interceptor is filtered to the three GET methods; it is never consulted for a POST, PUT or DELETE. Interceptors can also be switched on and off at runtime with :enable() and :disable() without removing them from the chain — useful for enabling verbose logging in a specific deployment.
Because RestInterceptor builds on the framework's resilience layer, each interceptor also participates in the service's health monitoring. A concern that misbehaves is observed by the same mechanism that supervises the rest of the microservice, rather than failing silently.
What you actually gain
The benefit is easiest to see as a ratio. With N endpoints and M cross-cutting concerns:
- Inline approach: up to N x M copies of infrastructure code, spread across every method.
- Interceptor approach: M interceptor classes plus N clean business methods — the two are added, not multiplied.
For the five-endpoint, four-concern service that meant twenty inline blocks; with interceptors it is four small classes. Adding a sixth endpoint costs one business method and zero concern code — authentication, logging and caching apply to it automatically the moment it is registered.
Concretely, for a LOB developer this means:
- Less code to write and read. Each concern exists once. Handlers shrink to their business intent.
- Consistency by construction. A new endpoint cannot "forget" the token check or the log line, because the check lives in the chain, not in the method.
- Cheap change. Reworking the log format, the cache strategy or the auth scheme touches one class, not twenty methods.
- Testable business logic. "Read a customer" can be tested on its own; the concerns are tested separately, once.
- Fewer defects. Boilerplate written once has one place to be wrong, and one place to fix.
This is not a new idea in the industry — it is the same separation that aspect-oriented frameworks and servlet filters provide in the Java world. What matters here is that Xbase++ V3 offers it as a first-class, typed class inside the microservice framework, so LOB code stays in Xbase++ and stays focused on the business.
A production-ready example
The snippets above are teaching-sized. A complete, runnable service that uses exactly this pattern is available as a public example for Xbase++ V3:
https://github.com/alaska-software/advanced-customer-endpoint
It is a REST microservice for customer data with full CRUD operations over a DBF/CDX database, and it ships three real interceptors registered in that order: LoggingInterceptor (request/response logging with timing), AuthInterceptor (JWT validation on protected endpoints) and CacheInterceptor (result caching for the read methods getAll, getById and getByName). Authentication is issued through a separate /auth/login endpoint that returns a bearer token; the auth interceptor then validates that token on the customer routes.
The example also shows how interceptors sit next to the other production concerns of a microservice — a stateless workarea container (WAContainer) for multi-threaded database access, a recovery manager, and CPU/memory health watchdogs — and it can run both as a console application and as a Windows service. The repository holds the current routes, the data schema and the build and deployment steps. It is the recommended starting point for turning the pattern shown here into a service of your own.
One caveat the example states plainly: its JWT signing secret is hard-coded for demonstration. A real deployment must load the secret from configuration or the environment, use password hashing and run over TLS. The interceptor pattern is production-shaped; the demo credentials are not.
Summary
Cross-cutting concerns — authentication, logging, caching, error handling — are necessary but do not belong inside business methods. Written inline they multiply across endpoints, drift out of sync and bury the logic that matters. The Xbase++ V3 RestInterceptor class isolates each concern in one place and runs it around the handler through three hooks — :before(), :after() and :nError() — with a three-way vote (commit, abort, ignore) that decides whether the method runs, is stopped, or is answered from elsewhere. The handler goes back to doing one job. The concern is written once and applied by registration and filtering. For LOB teams the result is less code, more consistency, cheaper change and fewer defects — the same engineering payoff that filters and aspects deliver in other stacks, available directly in Xbase++.