The Problem the Industry Hasn't Solved Yet
Most enterprise software vendors are solving a SaaS customisation problem with tools designed for on-premise delivery. The inheritance model — customers extending platform behaviour through Java class hierarchies, compiled and packaged alongside core — was the right answer for its era. Every customer ran their own installation. Upgrade timelines were theirs to own. The coupling between core and customisation was manageable because the delivery model absorbed it.
That era is over. SaaS delivery, continuous releases, and multi-tenant operations have changed the requirements completely. But the architecture most vendors are working with has not kept pace. The result is visible and quantifiable: upgrade projects that consume six to nine months of a medium-sized team's capacity, SaaS roadmaps constrained by the need to maintain backward compatibility across thousands of customer customisations, and customers who stay on old releases not because they want to but because upgrading costs too much.
This is not a problem any single vendor created. It is a structural property of successful, deeply adopted enterprise software — the kind that SAP, Oracle, Guidewire, and others have all built. I have been working on exactly this class of platform: a large-scale enterprise system managing complex business entities, workflows, and integrations across multiple lines of business, built over two decades and customised deeply by every customer who uses it.
The question is not whether the old model served its purpose — it did. The question is what replaces it, and why most attempts at replacement fall short.
Why the Obvious Solutions Don't Work
The first instinct is usually configuration. If customers can configure behaviour rather than extend it in code, the coupling disappears. This works up to a point — typically the point where a customer's requirement is genuinely novel and cannot be expressed through the options the platform anticipated. Configuration systems solve the common cases. They fail exactly when customers need them most.
The second instinct is a plugin system. Expose stable APIs, let customers implement them, load the implementations at runtime. Better — but a plugin system without enforced boundaries gradually accumulates plugins that reach into platform internals the API was never meant to expose. The coupling re-emerges, just less visibly. And in a multi-tenant environment where plugins from different customers run in the same process, one misbehaving plugin can affect every other customer on the instance.
The third instinct — the one most teams eventually reach — is microservices. Move customisation out of the monolith entirely. Make it someone else's deployment problem. This works for some use cases and fails for others. An extension that needs to participate in the platform's database transaction cannot run in a separate process. An extension that needs sub-millisecond latency cannot absorb a network round-trip. Microservices push the problem rather than solving it.
What is actually needed is a framework that satisfies constraints that pull in different directions simultaneously: extensions that can run in-process or out-of-process depending on their requirements, with a consistent programming model across both; tenant isolation that is enforced structurally, not by convention; hot deployment without downtime; and trust boundaries that the platform controls, not the extension author. Getting all of these right at the same time, on top of a live platform that cannot be taken offline, is where the hard work lives.
The First Decision: Explicit Over Implicit
The most consequential early decision is whether extension points are explicit or implicit.
Implicit extensibility — anything can be overridden, any class can be subclassed, any behaviour can be intercepted — looks maximally flexible. In practice it produces systems where the platform team has no stable contract to maintain, extension authors reach into internals never designed to be touched, and refactoring becomes dangerous because any rename or restructure might silently break an extension somewhere in a customer's codebase. The coupling is invisible until it breaks, and it always breaks at the worst time.
Explicit extensibility inverts this. Core developers deliberately mark which methods are extension points and define what each phase of execution can do. This feels more restrictive — and it is, intentionally. The restriction is the value. The platform owns a stable, versioned contract. Extension authors work against a documented surface. Both sides evolve independently within their boundaries.
The discipline of deciding which methods to expose also forces useful thinking. It surfaces questions that should be asked anyway: what is the intended behaviour of this method, what state is safe to share with an extension at this point, what happens if an extension here throws. Answering those questions at design time is far cheaper than discovering the answers in a production incident at a customer site.
The Interception Model
Once explicit extension points are the decision, the interception mechanism is the next critical choice — and this is where most teams make a mistake they later regret.
Proxy-based interception is the default. It is easy to implement, well understood, and supported by every major Java framework. It is also fundamentally limited in a way that matters enormously in enterprise codebases: a proxy wraps an object, not a class. Calls made from within the same class — this.method() — bypass the proxy entirely. In a system built over twenty years with deep internal call chains, this is not a theoretical edge case. It is a daily occurrence. Extensions register correctly, the logs show them loading, and they simply never fire.
Compile-time bytecode weaving rewrites the compiled class files directly. The interception point is in the bytecode itself — it fires regardless of how the method is called, externally, internally, through a superclass, through a delegation chain. The build pipeline is more complex. The behaviour is reliable. On a codebase that was not designed from the ground up with extensibility in mind, reliable beats elegant.
The execution model that follows is a three-phase system: logic that runs before the core operation, logic that replaces it entirely, and logic that runs after it completes.
The phase model also makes failure handling tractable. A PRE hook that throws can abort the operation cleanly before anything is written. A POST hook that throws can be handled independently of the core outcome. An OVERRIDE hook that throws owns the failure semantics entirely. Each case has defined, predictable behaviour — which means both extension authors and platform operators can reason about failure modes before they encounter them in production.
The Trust Problem
The hardest design question in extensibility is not technical. It is about trust.
The naive position is to trust extension authors to behave responsibly. This is reasonable for internal teams building extensions on a platform they also operate. It is not reasonable for a SaaS platform where extensions come from dozens of independent vendors and customers, built by teams with varying levels of experience, deployed into a shared environment where a failure in one extension can affect every other tenant on the same instance.
The alternative is to make the platform's boundaries enforced rather than conventional. The platform decides — not the extension author — what extension code can access, what it can modify, and what operations it can perform. If an extension attempts to reach outside its permitted scope, the platform stops it. Not with a code review comment. Structurally.
Two consequences follow from this.
First, enforcement needs to happen at multiple levels. Checking only at deployment means a buggy extension causes damage before the check runs. Checking only at runtime means the feedback loop for extension authors is slow and the discovery happens in a customer environment. The right model layers the checks: some during the extension's own build process, some when the extension registers with the platform, some at runtime as a final line. Each layer catches different failure modes. None of them alone is sufficient.
Second, state protection has to be explicit. When an extension runs in the same process as the core platform, it shares the heap. An extension that receives a domain object has a direct Java reference to that object. Without enforcement, it can modify that object — and the modification will be visible to whatever core logic reads it next. The mechanism for preventing this needs to be applied consistently at every point where objects cross the boundary from platform into extension code. Convention does not hold across hundreds of extensions from dozens of vendors over years of operation.
Multi-Tenancy: One Instance, Many Customers
This is where the extensibility framework intersects most directly with the SaaS business model — and where getting it wrong has the most visible consequences.
The goal is a single running application instance serving multiple customers simultaneously, each with their own active extensions, with complete isolation between them. A hook registered for customer A never fires for customer B. An extension update for one customer does not interrupt another customer's in-flight session. A new customer can be onboarded — extensions loaded, registered, made active — without restarting anything.
The architectural key is that tenant identity has to flow through the entire call chain automatically. Every incoming request carries a tenant identifier. Every hook lookup is scoped to it. The registry merges two sets at dispatch time: extensions that apply globally across all tenants, and extensions specific to the current customer. The merge is invisible to both the core application and the extension authors.
The layer model adds nuance that flat extensibility cannot represent. Enterprise platforms operate with multiple tiers — corporate standards that apply universally, regional rules that apply to specific markets, individual customer configurations that are the most specific of all. A flat model collapses these tiers and forces every customer to re-implement logic they never intended to own. A configurable hierarchy preserves the tiers, with deterministic resolution when layers conflict.
Hot-reload is non-negotiable in a SaaS context — and it is harder than it looks. Simply swapping the old extension for the new one risks interrupting executions that are partway through a hook invocation. The right approach tracks in-flight executions, waits for them to complete, then unloads the old code and loads the new code into the now-empty context. Other tenants are entirely unaffected. The operational benefit — zero-downtime deployment for every extension update — justifies the implementation complexity.
Two Runtimes, One Contract
One of the harder design goals is supporting both in-process and out-of-process execution with a single programming model. The temptation is to pick one and optimise for it. Both are wrong choices.
In-process execution is not optional for extensions that participate in the platform's database transaction. If an extension modifies data that the core operation is about to write, that modification must be part of the same commit or the same rollback. A network round-trip cannot be part of a transaction boundary. For these cases, in-process is the only correct answer.
Out-of-process execution is the right model for extensions that react to completed operations rather than participate in them. Notifications, downstream workflow triggers, audit writes — none of these need transactional coupling with core. Running them out-of-process gives them independent deployment, independent scaling, and complete isolation from the core platform's failure modes. Forcing them in-process is unnecessary risk.
The design decision that resolves this is to define the contract at the level of the extension author's experience, not at the level of the execution mechanism. Extension authors write to a single context API and declare their execution preference in metadata. The framework handles in-process invocation or network serialisation transparently. An extension author should not need to understand the difference between the two to write correct extension code.
Deferred post-commit execution eliminates an entire class of distributed consistency problems. An extension that declares it should fire after the transaction commits will never fire on a rollback — the platform guarantees this. If the extension itself fails after a successful commit, the failure is handled independently. The extension author states the intent. The platform owns the guarantee.
What Changes
The contrast with the inheritance model is not subtle.
For the platform team, a core release no longer requires coordinating with every customer's development team to analyse the impact on their customisations. The published extension point catalog is the contract. If a customer's extension compiles against it, the upgrade is compatible. If it doesn't, the incompatibility is visible immediately — not six months later during a migration project.
For customers, a business rule change that previously required a platform upgrade cycle can be deployed as an extension update — tested, validated, and live without touching the core system. New tenants onboard into a running instance without downtime for anyone else.
For the engineering organisation, the six-to-nine-month upgrade project becomes a compatibility check and a deployment step. The performance campaign that had to model the emergent complexity of deep inheritance hierarchies becomes per-extension metrics — latency, error rate, timeout rate — per tenant, in standard observability tooling.
The underlying shift is from coupling to contract. Inheritance couples extension code to core code permanently. A hook-based framework with explicit extension points, enforced boundaries, and versioned contracts decouples them — while keeping the flexibility that made the old model worth building in the first place.
The Tension That Remains
Designing this kind of framework surfaces a tension that does not fully resolve — it only gets managed.
Extension authors want maximum flexibility. Every constraint the framework imposes is, from their perspective, a limitation. Platform operators want maximum control. The tighter the boundaries, the more predictable the system's behaviour under load, under failure, and under a misbehaving extension.
Both positions are legitimate. The framework designer's job is not to pick a side but to find the boundary where the platform's constraints are structural — not guidelines that extension authors are expected to follow — while leaving genuine flexibility within that boundary.
Getting this wrong in either direction is costly. Too permissive, and the framework gradually accumulates extensions that reach into platform internals, recreating the coupling it was designed to eliminate. Too restrictive, and customers work around it through mechanisms the framework cannot see or control, which is worse than having the flexibility in the first place.
The goal is a framework where trust is architecturally guaranteed within a well-defined boundary. Not assumed. Not enforced by convention. Guaranteed by design.
Most enterprise platforms are still further from that goal than they publicly acknowledge. The inheritance model is being refined rather than replaced, and the cost continues to compound. The industry has the patterns it needs — explicit extension points, enforced boundaries, independently deployed tenant logic have all existed in various forms for decades. What is new is the scale and complexity of the platforms that need them, and the urgency of the SaaS transition that makes the status quo increasingly untenable.
That is the problem worth solving. And it is further from solved than most roadmaps suggest.
This article is Part 3 of the Incremental Modernization Architecture series.
Part 1: Enabling Observability in Legacy Systems
Part 2: Splitting Monoliths into Microservices Without Breaking the Business


Top comments (0)