TL;DR
- Application modernization strategy is the sequencing decision: which parts of a running product you change, in what order, and what evidence tells you it worked. For SaaS teams, the constraint is that customers stay logged in the whole time.
- Rewrites fail on SaaS products for a structural reason: the codebase carries years of accumulated behavior that no specification captured, and customers notice the missing pieces before your tests do.
- We assessed Discourse, a Rails product sold as hosted SaaS and also self-hosted by thousands of communities. It scored 72/100, with one dimension far below the rest.
- Structural coupling scored 55/100, with a dependency balance of 0.36 and 504 layer violations. Those numbers say the architecture diagram no longer predicts what a change will break.
- The second finding matters more for SaaS: 217 exposed endpoints with authorization logic spread across controller macros, before_action filters, and inline guardian calls. Correct, but expensive to audit.
- The strategy that works is the ratchet: apply the new standard to everything new and changed, block regressions in CI, then backfill the legacy surface in priority order.
Every SaaS engineering team eventually inherits a codebase that works well and changes badly. Features ship slower each quarter. Framework upgrades get postponed. Security review takes a week because nobody can answer “which endpoints are protected?” without reading controllers one by one.
The instinct is to rewrite. The better move is usually to modernize in place, with a sequencing plan that protects the running product. This post walks through what that plan looks like, using a real assessment of Discourse as the working example: what the scan found, why each finding specifically costs a SaaS team, and the order the work has to land in.
What application modernization strategy actually decides
An application modernization strategy is the set of sequencing decisions that govern how a running product evolves. Not which framework to adopt, and not whether to refactor, but the ordering: what moves first, what gates each step, and what evidence closes it out.
Four decisions carry most of the weight:
- Scope boundary: Which surfaces change this phase, and which are explicitly deferred. Undefined scope is the most common reason modernization programs stall.
- Enforcement direction: Whether new standards apply to everything at once, or to new and changed code first, with legacy backfilled later.
- Continuity guarantees: What must not break, stated as measurable thresholds rather than intentions.
- Evidence contract: What proves a phase is done: tests passing, CI gates active, monitoring thresholds held.
Modernization differs from both rewriting and routine refactoring. A rewrite replaces the system and concentrates risk at cutover. Refactoring improves code without a program around it. Modernization sits between structural change and sequencing deliberately, with continuity guarantees attached.
| Approach | Risk profile | Fits when |
| Rewrite | Concentrated at cutover | The product is being repositioned, or the stack is genuinely unsupportable |
| Modernize | Distributed across many verified steps | The product works but changes slowly, and upgrades keep slipping |
| Refactor | Low, but unbounded | Improvement is local, and no framework or contract shift is needed |
Why SaaS products resist the standard rewrite path
Teams outside SaaS can often run a replacement alongside the original and migrate gradually. Three properties of SaaS delivery make that harder.
Customers are mid-session, continuously. There is no maintenance window when no one is using the product. A cutover that drops sessions, invalidates tokens, or changes API response shapes immediately generates support load, and for a self-hosted product, it generates it across installations you do not control.
Behavior accumulated without documentation. A mature SaaS codebase encodes years of decisions: this filter behaves that way because a customer needed it; this endpoint accepts an extra parameter because an integration depended on it. A rewrite built from specification implements the documented behavior and silently drops the rest.
Extensions depend on internals. Products with plugin ecosystems have third-party code reaching into internal structure. Discourse ships bundled plugins and supports a large community ecosystem. Changing internals can break extensions that customers depend on, and the core team often has no visibility into what is deployed where.
| Constraint | Rules out | Permits |
| Continuous customer sessions | Big-bang cutover, extended downtime | Phased rollout, canary cohorts, feature-flagged change |
| Undocumented accumulated behavior | Rewrite from specification | Contract tests capturing current payload shapes |
| Third-party plugin dependencies | Freely restructuring internals | Stable published contracts with internals refactored beneath |
| Self-hosted installations | Coordinated cutover across all users | Backward-compatible releases with documented migration windows |
Reading a modernization assessment of a real SaaS codebase
To ground this, we ran Discourse through Forge, which scans a repository and scores it on eight dimensions of how difficult it is to change the system safely.
Discourse is a useful subject because it is genuinely commercial. The company sells hosting at discourse.org, the code is open source, and thousands of organizations self-host it. It is a Rails monolith with an Ember frontend, 571 files analyzed, and an active plugin ecosystem.
The composite came back 72 out of 100, rated “Established.” The dimension spread carries more signal than the composite.
| Dimension | Score | Rating | What it measures |
| Hidden Gems | 84 | Advanced | Reusable patterns worth extracting |
| Logic Narrative | 78 | Advanced | How readably the code tells its story |
| Trust Boundaries | 74 | Established | Where untrusted input crosses into the system |
| Semantic Clarity | 72 | Established | Whether names match domain concepts |
| Data Weight | 71 | Established | How efficiently data moves |
| Future Proofing | 70 | Established | Resilience to future change |
| Cognitive Load | 66 | Established | Mental effort a change requires |
| System Gravity | 55 | Developing | How concentrated the structural mass is |
A 17-point gap separates System Gravity from the next-lowest dimension. That gap is the strategy input: it says the code reads well and the domain language is sound, but the structure resists safe change.
The eight dimensions as Forge reports them. The 17-point gap below System Gravity determines the sequencing.
Three findings that shape the sequencing decision
The scan surfaced ten findings. Three of them determine the ordering, because fixing them changes what every later step can safely assume.
Structural coupling makes change blast radius unpredictable
System Gravity measures how evenly responsibility is distributed. Discourse scored 55, with a dependency balance of 0.36, where an even graph approaches 1.0.
Two specifics drive it. Circular dependencies exist when module A depends on B and B depends on A. Cycles force both modules to be understood and changed together, which is why the scan classifies them as high-severity future-change blockers rather than style issues.
504 layer violations were detected, meaning code at one architectural level reaches directly into another. Some are benign inherited calls. Collectively, they mean a developer cannot use the architecture diagram to predict what a change affects, so every change requires manual blast-radius analysis.
Distributed authorization makes security review expensive
The scan found 217 exposed endpoints with authorization present but expressed through several different idioms: controller macros, before_action filters, inline guardian calls, and local checks inside actions.
217 endpoints were reviewed by reading code, versus the same endpoints reviewed by comparing declarations against behavior.
Nothing here is wrong. The finding is about auditability, not vulnerability. When a reviewer asks whether a new endpoint is correctly protected, the answer requires reading the controller, its parent classes, its filters, and any inline checks. Multiply by 217, and quarterly security review becomes a week of work that produces no product value.
The proposed fix is a declarative trust manifest: each endpoint records its intended login requirement, guardian policy, rate limit, CSRF posture, and redirect contract. Review then compares the declaration against behavior rather than reconstructing intent from code.
Cross-host redirects concentrate risk in one flow
SessionController#sso and #sso_provider use redirect_to … allow_other_host: true for DiscourseConnect, the SSO integration. That is a legitimate SSO pattern, and the flow is a deliberate design choice rather than an oversight.
The risk is that its safety depends on upstream payload validation staying correct. There is no local assertion in the redirect code itself declaring which destinations are permitted, so a future change to validation elsewhere could turn a working SSO flow into an open redirect without any local code looking wrong.
Each finding becomes a work order with explicit blockers. The trust schema cannot start until the architecture seam map is baselined.
Building the ratchet that stops debt from growing
The remediation for all three findings shares one mechanism, and it is the single most transferable idea for SaaS teams: the ratchet.
A ratchet applies a new standard to everything new and changed, blocks regressions in CI, then backfills the existing surface in priority order. It works because it separates two problems that teams usually conflate: stopping the debt from growing, and paying down what already exists. The first is cheap and immediate. The second is expensive and can proceed at whatever pace capacity allows.
The generated plan applies it three times:
- Architecture ratchet: WO-003 baselines the seam map. WO-014 adds a CI check blocking new dependency cycles. Existing approved debt is tracked separately from new debt, so the baseline is not a pass/fail gate on work that predates it.
- Trust ratchet: WO-004 defines the endpoint trust schema, WO-016 generates an inventory of current state, WO-017 adds controller annotations, and WO-028 gates coverage in CI. New and changed endpoints must declare their trust posture; legacy endpoints will be backfilled later under WO-048 and WO-054.
- Dependency ratchet: WO-052 enforces that frontend date dependencies do not regress after the migration in WO-044.
The target is stated as zero new violations per release rather than zero total violations. That distinction is what makes the gate acceptable to enable early, and gates that get enabled early are the ones that survive.
The SSO work follows the same discipline in a different form. WO-005 baselines the current redirect contracts before anything changes, WO-018 centralizes destination validation, and WO-026 explicitly declares the trust contracts, with regression tests proving that valid integrations still complete.
Sequencing phases so customer-facing flows stay stable
The rollout runs in five phases from August 2026 into 2027, and ordering follows ratchet logic directly.
| Phase | Window | Focus |
| 1. Baseline and guardrails | Aug 25 – Sep 30 | Seam map, trust schema, runtime targets, rollback criteria |
| 2. New and changed surface enforcement | Oct 1 – Oct 31 | CI ratchets active, Node 24 enforced, trust checks on new endpoints |
| 3. Framework and runtime upgrade | Nov 1 – Nov 30 | Rails 8.1 adoption, Rack compatibility, SSO regression suite |
| 4. High-risk legacy backfill | Dec 1 – Dec 31 | Top 5 list/topic contracts, legacy endpoint trust backfill |
| 5. Controlled expansion | From Jan 2027 | Broader coverage, serializer modernization, plugin program |
Phases 1 and 2 do not change product behavior. They install governance. Only in Phase 3 does the framework upgrade land, and by then the ratchets are already catching regressions the upgrade might introduce.
The go/no-go gates are stated in operational terms rather than engineering ones:
- SSO login completion at or above 99.9% during rollout windows
- Admin configuration availability at or above 99.9%
- Fewer than 2 Sev-1 or Sev-2 regressions per phase
- Zero new dependency-cycle violations per release from November 30
Stopping new debt and paying down old debt are separate problems. The ratchet lets the first ship in weeks while the second proceeds at whatever pace capacity allows.
Those thresholds mean a phase can be blocked by production monitoring, not only by failing tests. For a product with paying hosted customers and self-hosted installations, that is the correct place to put the tripwire.
Two months of governance before any framework change lands.
Applying this framework to your own SaaS codebase
The specifics come from a single Rails product, but the sequencing logic applies to most mature SaaS systems.
- Score before you scope: The 17-point gap between System Gravity and everything else told us structural coupling outranked readability work. A task list written from intuition would likely have started somewhere less useful.
- Ratchet before you refactor: Stopping new debt is cheap and can ship in weeks. Paying down existing debt is expensive and can proceed at whatever pace capacity allows. Conflating them delays both.
- Make trust declarative before you audit it: 217 endpoints reviewed by reading code is a week of work per cycle. The same endpoints reviewed by comparing declarations against behavior is an afternoon.
- Baseline security-sensitive flows before touching them: The SSO work first captures the current redirect contracts, then hardens them. Reversing that order means discovering which integrations you broke after they break.
- State gates in operational terms: SSO completion above 99.9%” is a threshold your monitoring already tracks. Code coverage percentages are not something customers notice.
Well-done modernization produces no visible change for customers. What it produces is a team that can take the next framework upgrade without postponing it another two quarters.
FAQs
When should a SaaS team modernize rather than rewrite?
Modernize when the product works commercially but changes slowly. The signals are framework upgrades being repeatedly postponed, security reviews taking days, and feature estimates increasing for comparable work. Rewrite when the product is being repositioned into a different market, or when the stack is genuinely unsupportable, meaning no security patches and no hiring pool. Discourse fits the first case: a working commercial product carrying structural drag, where the cost is velocity rather than viability.
What is a modernization ratchet and why does it work?
A ratchet applies a new standard to new and changed code, blocks regressions in CI, then backfills legacy code over time. It works because it separates stopping the bleeding from healing the wound. Blocking new dependency cycles can ship in a sprint. Retiring existing cycles across a large codebase takes quarters. Teams that wait to enforce until the legacy surface is clean usually never enforce at all, because the cleanup never finishes.
How do we modernize without breaking third-party plugins?
Keep public contracts stable and move internal logic under them. In this plan that means publishing typed plugin contracts, adding contract tests for bundled plugins, and validating a representative sample of community plugins before release. Plugin compatibility is treated as a release gate, so changed paths must compile against the published contract before shipping. Silence from plugin authors is not evidence of compatibility; a passing test suite is.
How long does a SaaS modernization program take?
The Discourse plan runs about five months for the first four phases, with expansion continuing into the following year. The useful signal is the proportion rather than the duration: two months to establish governance before any framework change, one month for the upgrade itself, then a month of legacy backfill. A plan that allocates most of its time to code changes and little to governance and verification usually underestimates the cost of changing a running product.
What evidence proves a modernization phase is actually complete?
Evidence the system produces rather than a status update. In this plan, that means CI gates are active and passing, contract tests cover the targeted flows, trust declarations are present on all new and changed endpoints, and production monitoring remains above the stated thresholds throughout the rollout window. Each phase names its gates in advance, so completion is a check against criteria rather than a judgment call at the end.