Java consulting, architecture and modernisation

Java runs a disproportionate share of the systems that cannot fail — core banking, telecom mediation, insurance administration. The engineering question is rarely whether to keep it, and almost always how to keep it changeable.

Overview

Java's position in enterprise estates is a consequence of longevity. It has been the default choice for large business systems for long enough that the most consequential software in banking, telecommunications and insurance frequently runs on it — often on codebases older than the teams maintaining them.

The runtime itself has improved substantially, and modern Java is a considerably better language and platform than the version most legacy estates are written against. The gap between what a codebase uses and what the platform now offers is usually wide.

Our work here is mostly modernisation: moving off end-of-life runtimes and frameworks, decomposing where decomposition genuinely helps, and restoring the ability to change a system the business cannot pause. Replacement is occasionally right and is proposed far more often than it is warranted.

Enterprise use cases

Where we see this used to good effect, and what makes each case a fit.

Core transactional systems

Banking, insurance and telecom platforms where correctness and stability outrank development speed, and where the ecosystem's maturity is the point.

High-throughput processing

Mediation, settlement and event processing at volumes where the JVM's performance characteristics are well understood and predictable.

Long-lived platform modernisation

Estates on end-of-life runtimes or frameworks that need a supported path forward without a rewrite.

Enterprise integration

Where the integration and messaging ecosystem is mature and the operational patterns are well established.

Data-intensive back ends

Services close to large relational estates, where the driver and tooling ecosystem is deepest.

Regulated workloads

Systems where audit, determinism and long-term supportability matter more than adopting the newest available option.

Architecture

The architectural question in most Java estates is decomposition: what, if anything, should be split out of a large application that works but is slow to change. The honest answer is frequently less than proposed, because the cost of distribution is paid continuously.

The second is framework coupling. Long-lived Java applications are usually coupled to a framework generation, and the cost of moving is dominated by how deeply that framework reached into business logic rather than by the framework change itself.

  • Decomposition justified by independent change or scale, not by architectural fashion
  • Business logic insulated from framework specifics so a version move is feasible
  • Module boundaries enforced by build structure rather than by convention
  • Transactional boundaries stated explicitly, since correctness depends on them

Best practices

The practice with the largest payback in older Java estates is restoring build and dependency hygiene. Estates that have drifted frequently cannot upgrade anything because the dependency graph has conflicts nobody has resolved for years.

Beyond that, the usual disciplines apply: tests fast enough to run continuously, static analysis in the build rather than in review, and explicit rather than implicit configuration.

  • Dependency graph resolved and pinned, since it blocks every other improvement
  • Static analysis and style enforced in the build rather than requested in review
  • Tests fast enough to run on every change, or they will not be run
  • Configuration explicit and externalised, with no environment-specific builds

Security

Java's most consistent security exposure is dependency provenance. Enterprise Java applications carry large transitive dependency graphs, and a serious vulnerability in a widely-used library becomes an estate-wide emergency precisely because nobody has an accurate inventory.

The second is deserialisation and dynamic configuration, which have produced a series of high-impact vulnerabilities and remain a category worth designing against rather than patching reactively.

  • An accurate dependency inventory, so an advisory is a query rather than a hunt
  • Deserialisation of untrusted input avoided by design, not filtered
  • Runtime and framework versions kept current enough to receive patches
  • Secrets from a managed store or workload identity, never from packaged config

Scalability

The JVM scales predictably, and the constraints in Java estates are usually the database and the thread model. Applications written against a thread-per-request model have a concrete ceiling, and reaching it produces queueing rather than a clean failure.

Heap sizing and garbage collection choice matter at scale and are frequently set from folklore rather than measurement, which produces either wasted memory or pauses in exactly the workloads that cannot tolerate them.

  • The database assessed as the constraint before the application tier is scaled
  • Thread model understood, since thread-per-request has a concrete ceiling
  • Heap and garbage collector configured from measurement rather than convention
  • Connection pool sizing matched to the database's actual capacity

Performance

Java performance work rewards measurement more than most platforms, because the runtime optimises aggressively and intuition about what is expensive is frequently wrong. Profiling under realistic load is the only reliable starting point.

Garbage collection pauses are the classic issue and are usually addressed by reducing allocation rather than by tuning the collector — though for latency-sensitive workloads the collector choice does genuinely matter.

  • Profiling under realistic load before any optimisation is attempted
  • Allocation reduced in hot paths ahead of collector tuning
  • Collector selected against the latency requirement, where one is stated
  • Data access examined first, since it usually dominates runtime cost

Integration

Java's integration ecosystem is the most mature of any platform we work with, which is genuinely valuable and occasionally leads to integration frameworks being adopted for problems that a plain interface would solve.

In large estates the more consequential issue is contract discipline. Java systems are frequently integrated through shared libraries, which couples release cycles across teams in ways that are difficult to unpick once established.

  • Shared libraries across service boundaries avoided, since they couple releases
  • Interface contracts specified including error semantics and versioning
  • Messaging semantics stated per flow: ordering, delivery and idempotency
  • Integration frameworks adopted only where a plain interface genuinely will not do

Operations

Operating Java is well-understood territory, and the recurring issue in older estates is runtime lifecycle. Long-term-support releases have defined support windows, and estates running unsupported versions cannot receive security patches at all.

Observability is the second. Older applications frequently log in formats that predate structured logging, which makes correlation across services an exercise in text parsing rather than a query.

  • Runtime on a supported release, because otherwise patches are unavailable
  • Structured logging with correlation, replacing formats that predate it
  • JVM metrics exported and alerted on impact rather than on utilisation
  • Graceful shutdown so deployments do not drop in-flight work

Governance

Governance in Java estates is largely dependency and version governance, because that is where the risk concentrates. An organisation that cannot state which applications use a given library cannot respond to an advisory in useful time.

The second is consistency across teams. Shared build templates and agreed framework versions reduce the cost of moving engineers between codebases substantially, provided the standard is genuinely easier to follow than to bypass.

  • A dependency inventory the organisation can query when an advisory lands
  • Runtime and framework versions agreed, with exceptions documented rather than silent
  • Shared build templates that are easier to adopt than to work around
  • Architectural decision records kept, since these systems outlive their authors

Frequently asked questions

Modernise, in most cases we assess. Replacement is proposed considerably more often than it is warranted, and it discards working behaviour that is frequently undocumented — which is exactly the behaviour a replacement then has to rediscover under time pressure.

The security position is the immediate concern: unsupported runtimes do not receive patches at all. The second effect is compounding — each version skipped makes the eventual upgrade harder, so the cost of waiting grows rather than staying constant.

There, before anything else. A conflicted dependency graph blocks every other improvement, including security patching. It is unglamorous work and it is usually the highest-value first step in a drifted Java estate.

Only where a component genuinely needs to change or scale independently. Distribution costs are paid continuously — deployment, monitoring, network failure handling, data consistency — and a monolith that is slow to change because of coupling will be slow to change as services too.

The determining factor is whether you have an accurate dependency inventory. With one, the response is a query and a patch. Without one, it is a hunt across every codebase while the clock runs — which is what turns an advisory into an incident.

For long-lived transactional systems, high-throughput processing and regulated workloads, yes. The ecosystem maturity and operational predictability are the point. For rapid product iteration or where the libraries you need sit elsewhere, other choices frequently fit better.

Possibly, and it is worth measuring rather than assuming. Where it is, reducing allocation in hot paths usually helps more than tuning the collector — though for genuinely latency-sensitive workloads the collector choice does matter and is frequently set from folklore.

Yes, with attention to resource limits and heap sizing, which interact in ways that surprise teams the first time. Modern runtimes are container-aware, but limits set without measurement produce either wasted memory or unexpected termination.

Sparingly, and not across service boundaries where independent release matters. Shared libraries couple release cycles, so a change to a common library becomes a coordinated release across teams — which is precisely what splitting into services was meant to avoid.

Structured logging with a correlation identifier propagated across boundaries. Older Java applications frequently log in formats predating structured logging, which makes any cross-service investigation an exercise in text parsing rather than a query.

Both. Delivery runs through software development, and we also take review and advisory engagements where your team builds and we assess design and modernisation sequencing.

Next step

Discuss a Java estate

A free first consultation covering your runtime and framework position, what modernisation would actually involve, and whether replacement is being proposed for reasons that survive examination.

No cost, no obligation. Pricing is transparent and includes GST.