Hire Java Developers in India
When you hire Java developers in India through us, you are usually not staffing a blank repository. You are staffing a system that already exists, that somebody else designed, that earns money today and cannot be switched off while it is improved. That is the job we screen for.
Java hiring pages tend to open with the greenfield fantasy: a clean Spring Boot service, a fresh domain model, an architecture diagram nobody has argued about yet. A small share of Java work looks like that. The rest looks like an order management system written across three teams over nine years, a build that only one person fully understands, a Hibernate mapping that generates four hundred queries on the reporting screen, and a payment reconciliation job that must not be late. This page describes the engineer who is good at the second thing, because that is who most buyers actually need and rarely know how to interview for.
What a Java Engineer Does in a Normal Week
The honest split, across the briefs that reach us, is roughly one week of new feature work for every three weeks of change inside something that already runs. Say that out loud in your job description and you will attract a different and better candidate than the one who turns up expecting a clean slate.
Changing code they did not write
A typical ticket is not build a service. It is add a field to an order, which turns out to touch an entity, a DTO, two mappers, a validation group, a database migration, a Kafka payload that a downstream team consumes, and a report query that assumed the old column list. The skill is tracing that blast radius before touching anything, and the way you spot it in an interview is watching someone open an unfamiliar repository and follow a request from controller to database without narrating a rewrite. People who cannot resist proposing a rewrite in the first hour are expensive on maintenance work.
Diagnosing behaviour, not just implementing behaviour
A large slice of the week is answering questions that begin with why. Why does this endpoint take nine seconds only for the largest tenant. Why did the nightly job stop finishing since the release on the fourteenth. Why does the service need a restart every Tuesday. These are the questions that separate a Java engineer from a Java coder, and they are answered with logs, a profiler, a heap dump and a query plan rather than with intuition.
Living inside the build and the dependency graph
Java projects accumulate dependencies the way garages accumulate boxes. Somebody upgrades one library, a transitive version shifts underneath four others, and a method that existed at compile time is gone at runtime. Engineers who have carried a real application through a framework upgrade treat the dependency tree as part of the codebase. Engineers who have not treat it as configuration that somebody else owns, and you find out which kind you hired the first time a build fails on a machine that is not theirs.
Writing tests around code that was never designed to be tested
Legacy Java is full of static calls, constructors that reach out to the network, and classes with eleven collaborators wired by field injection. Adding a test to that is a design exercise before it is a testing exercise: extract an interface, move construction to the caller, break one dependency so the rest can be faked. Candidates who insist on ninety percent coverage before they will touch anything have not worked in this world. The ones who add a characterisation test around the bit they are about to change have.
Being the person the operations channel pings
Java services in most organisations are the ones with money flowing through them, which means the Java team is the team paged when something stops. A Java engineer with real production experience talks about their alerting, their dashboards, what their p99 looked like before and after a change, and the incident where their first hypothesis was wrong. If a candidate has never seen their own code fail in production, they are not necessarily weak, but they are junior in a way years of experience will not tell you.
Should You Rewrite That Java Monolith or Keep Feeding It?
This question arrives inside about half the Java briefs we receive, usually phrased as a decision that has already been made. It is worth slowing down, because the answer changes which engineer you should be hiring.
The monolith is usually not the actual problem
Teams describe a slow, fragile, frightening application and diagnose the architecture. More often the pain comes from four things that have nothing to do with the shape of the deployment: a test suite that takes fifty minutes so nobody runs it, a release process that needs a person and a checklist, database access spread through the code with no clear boundary, and a data model that has been extended rather than corrected for years. Split that into services and you get the same four problems, distributed, plus network calls between them. Fixing the release pipeline and the test suite first is unglamorous and frequently buys more than a year of architectural work.
When splitting genuinely pays
There are real reasons. One part of the system needs to scale on a completely different axis from the rest, for example a search or pricing component that needs ten times the instances at peak. Two teams are blocked on each other's release train. A component has regulatory or data residency requirements the rest of the application does not. Or a single module fails often enough that its blast radius has to be contained. Those are architecture problems and they justify architecture work. If your reason is that services are modern, you are about to pay for a distributed system and receive nothing back. Where the case is real, the design questions worth settling before any code moves are covered on our microservices architecture services in India page.
The strangler approach, and what it costs
The sane way to split a Java monolith is incremental: put a routing layer in front, move one bounded piece of behaviour out, run both paths and compare, then cut over and delete the old code. The part people underestimate is the data. Two systems reading the same tables is not a migration, it is a coupling with extra latency, and the honest version means either moving the data with the service or accepting a period of dual writes with reconciliation. Ask a candidate how they kept two stores consistent during a cutover. Anyone who has actually done it will start talking about idempotency and replay rather than about service boundaries.
Who to hire for each answer
If you are keeping and improving the monolith, hire someone who is genuinely good at reading code, refactoring under test, and improving a build. That person is often undervalued by the market because their work does not photograph well. If you are splitting, hire someone who has run a strangler migration to completion, including the boring middle where both systems are live. The engineer who has only built new services inside an existing platform is a different profile again, and putting them on a nine-year-old codebase tends to end in a frustrated resignation on both sides.
Spring and Spring Boot: What Should a Candidate Actually Understand?
Almost every Java CV lists Spring Boot. It tells you very little, because Boot is designed so that a beginner can produce a working service quickly. The questions below are the ones that reveal whether someone understands what the annotations are doing.
Dependency injection is easy to use and easy to abuse
Constructor injection with final fields is the version we want to see. It makes dependencies visible in the signature, makes the object impossible to construct in an invalid state, and means the class can be instantiated in a test without a container at all. Field injection with an annotation on a private field looks tidier and hides the exact thing you want visible, which is that this class now depends on nine other things. When a constructor grows past four or five parameters, that is information rather than an inconvenience, and the fix is to split the class instead of switching to field injection to hide the count.
Two other things worth probing. Bean scope: nearly everything is a singleton, so a mutable field on a service is shared across every concurrent request, which is the source of a specific class of bug that only appears under load. And circular dependencies: a container that resolves an A to B to A cycle for you is not solving a design problem, it is postponing one, and a candidate who shrugs at a cycle will leave you a graph nobody can reason about.
Transaction boundaries and the Transactional traps
This is our single most productive Spring question, because the annotation is proxy based and the proxy is invisible in the source. Calling an annotated method from another method of the same class goes straight through the object and skips the proxy entirely, so no transaction begins and nobody gets an error telling them so. The same applies to a private method. People discover this when a rollback silently fails to happen, months after the code shipped.
Rollback rules are the second trap. By default the transaction rolls back on unchecked exceptions and not on checked ones, so a service that declares a checked exception and expects the database to unwind will commit half its work. The third is scope: a transaction that is opened at the top of a request and stays open while the code makes an outbound HTTP call holds a database connection hostage for the duration of somebody else's timeout. Under load that empties the connection pool and the whole application stalls on something that has nothing to do with the database.
Then there is propagation. REQUIRES_NEW starts a genuinely separate transaction on a separate connection, which is what you want for an audit record that must survive a rollback, and is also how a service deadlocks against itself when the outer transaction holds a row lock the inner one needs. A candidate who can describe both sides of that has been somewhere interesting.
JPA and Hibernate, where most of the slowness lives
The N plus one is the famous one and it is still everywhere. You load two hundred orders, the code touches order.getCustomer() inside a loop, and Hibernate issues two hundred and one queries. Each is fast, the endpoint is not, and nothing in the Java source hints at it. The fixes are known: a fetch join, an entity graph, batch fetching. The skill is noticing at all, which is why the only reliable answer is to log the generated SQL with counts during development and treat a jump in query count as a failing test rather than a curiosity.
Lazy loading is the related trap. A collection mapped lazily is a proxy, and touching it after the persistence context has closed throws. The usual patch is to keep the session open for the whole web request, which Spring Boot enables by default and which quietly turns your view rendering into a query generator. Turning it off is the right call for most services and it will surface a set of latent bugs on the day you do it, so the change belongs early in an engagement rather than in the week before a launch.
Two more that come up in real code. Pagination combined with a collection fetch join cannot be done in SQL, so Hibernate fetches the whole result set and paginates in memory while writing a warning to the log that nobody reads, and the endpoint works fine until the table grows. And entity equality: generated identifiers are null until flush, so an entity added to a hash-based collection before it is saved becomes unfindable afterwards. Ask a candidate how they implement equals and hashCode on an entity. There is no perfect answer, and the good ones say so before choosing one.
Why an ORM makes easy things easy and hard things obscure
This is the honest summary of JPA and it is worth saying out loud to whoever is buying. Loading an object graph and saving it back is genuinely less code than the alternative. The cost is that the SQL is generated somewhere you cannot see, and the moment your problem is a reporting query across six tables, or a bulk update of half a million rows, or an upsert with database specific syntax, the abstraction stops helping and starts hiding the thing you need to control. Dirty checking will also issue updates you never asked for, because an entity that was modified inside a transaction is flushed whether or not you called save.
The engineers we rate treat this as a boundary rather than a religion. Entities for the transactional write path where object identity and change tracking earn their keep, and plain SQL through a query library or a JDBC template for reporting and bulk work, mapped straight into read models. Candidates who insist that every access must go through the ORM tend to produce the four hundred query screen. Candidates who cannot read a generated query at all cannot diagnose it either.
Autoconfiguration, and knowing how to see under it
Spring Boot's value is that it configures a great deal for you based on what is on the classpath. Its risk is that engineers stop being able to explain their own application. The practical test is whether someone knows how to ask the framework what it decided: the condition evaluation report that lists which autoconfigurations matched and which were skipped and why, and the property resolution order that determines whether the environment variable or the profile-specific file wins. An engineer who has debugged a bean that existed in one environment and not another knows exactly where to look. One who has only ever added a starter and hoped will be stuck for a day on something that takes four minutes.
The JVM Is the Centre of How We Screen
Framework knowledge can be picked up in a month on the job. Understanding what the runtime is doing underneath cannot, and it is the difference between an engineer who fixes the memory problem and one who raises the limit and waits for it to happen again. Everything in this section comes up in our interviews.
Heap, stack, and where the object you are holding actually lives
Every thread gets its own stack, which holds frames for the methods currently executing along with local primitives and references. Objects themselves live on the shared heap. That single distinction explains a lot of behaviour that otherwise looks arbitrary. Deep or accidentally infinite recursion exhausts a stack and you get a StackOverflowError, which is per thread and unaffected by how much heap you have. Retaining too many objects exhausts the heap and you get an OutOfMemoryError, which is process wide and unaffected by stack size.
It also explains why a thread is not free. Each one reserves stack space, so a naive one-thread-per-connection design runs out of memory long before it runs out of CPU. And it explains why passing a large object around costs nothing in Java, because you are passing a reference, while creating that object in a hot loop costs a great deal. We ask candidates where the fields of an object live when the object is a local variable. The answer is the heap, and a surprising number of experienced people hesitate.
Garbage collection, and what a pause looks like from outside
Collectors in the HotSpot family are generational: most objects die young, so the collector scans a small nursery frequently and the older region rarely. G1 has been the default for many releases and works in regions, aiming to meet a pause target by collecting only as much as it can in the time allowed. The low-latency collectors, ZGC and Shenandoah, do far more of their work concurrently with the application and are the right choice when tail latency matters more than raw throughput. Choosing between them is a real decision and it should be driven by a measurement, not by a blog post.
What matters commercially is what a pause looks like to your users. Collection pauses do not show up in average response time. They show up in the p99, as a request that took eleven seconds while the median stayed at forty milliseconds, and in a load balancer that marks a healthy instance as failed because the health check timed out during a full collection. If your monitoring only records averages, you cannot see this at all. The first thing we would ask of a Java system with mysterious intermittent slowness is to turn on garbage collection logging and line the pauses up against the latency graph. That is a twenty minute job and it settles the question.
Two details separate people who have done this. Long pauses are not always collection: a safepoint that takes a long time to reach, because one thread is stuck in a long counted loop, stops everybody without the collector doing anything. And in a container, a JVM that is unaware of the cgroup limit will size its heap against the host and get killed by the kernel with no Java error at all, which is why a container OOM kill with an empty log is such a common and confusing incident.
Heap dumps and what they actually tell you
A heap dump is a snapshot of every object alive at a moment, and it is the only tool that reliably answers what is holding this memory. Configure the JVM to write one automatically when it runs out of memory, because the dump you did not take during the incident is the one you needed. Reading it, the distinction that matters is shallow size against retained size: shallow is the object itself, retained is everything that would be freed if it went away. The dominator tree sorted by retained size usually names the culprit within a minute.
The patterns repeat. A static map used as a cache with no eviction and no bound. A ThreadLocal set on a pooled request thread and never cleared, so the value outlives the request and accumulates. Listeners registered and never removed. A collection field on a long-lived object that only ever grows. Session state kept in memory in an application that now runs on six instances. Every one of these is invisible in the source code and obvious in a dump. That is precisely why we put a real dump in front of candidates rather than asking them to describe the theory of garbage collection.
Alongside dumps, Java Flight Recorder is the tool we expect a senior engineer to name. It records allocation, locks, exceptions, garbage collection and thread states from a running process with low enough overhead to leave on, and it answers the question a dump cannot: what was happening over time rather than at one instant. For CPU profiling under load, async-profiler is the common choice because it does not suffer the safepoint bias that older sampling profilers do.
Thread pools, and where deadlocks come from
Most Java concurrency in application code is not raw threads, it is a pool with work handed to it, and the defaults are where the trouble starts. A fixed pool created through the convenience factory method comes with an unbounded queue, so when work arrives faster than it is processed the queue grows until the heap is gone. There is no rejection, no backpressure and no warning. Configuring the executor directly with a bounded queue and an explicit policy for what happens when it is full is four extra lines and converts a memory failure into a load-shedding decision you chose.
Deadlocks in Java come from a small number of shapes. Two locks taken in different orders by two threads is the textbook one and the rarest in practice. Far more common is pool starvation: a task running on a pool submits another task to the same pool and waits for the result, and when every thread in the pool is doing that, nothing can ever finish. The same thing happens when blocking work is dropped into the shared pool that parallel streams use, since one badly placed blocking call there degrades unrelated parts of the application. A thread dump makes all of this visible instantly, and the deadlock detector will name the two threads and the two monitors for you. We ask candidates to read a thread dump. It is a fifteen minute exercise and it is remarkably hard to fake.
Why adding memory is usually the wrong first move
The instinct when a service dies with an OutOfMemoryError is to raise the heap. Occasionally that is right, because the workload genuinely grew. Usually it converts a crash on Tuesday into a crash on Friday, and it makes the eventual full collection longer because there is more to scan. It also masks the actual defect, which is almost always retention rather than volume: something holds references it should have released.
The sequence we want to hear is take the dump, find what is retained, fix the retention, then size the heap deliberately with a limit that respects the container. A related honest point is that a bigger machine hides an inefficiency at a cost that recurs every month, forever, across every environment. Fixing the cache without an eviction policy takes an afternoon. We have seen more money spent on instance sizes than the fix would have cost several times over, and it is one of the reasons a maintenance-focused engineer earns their keep quickly.
Modern Java, and What a Java 8 Habit Tells You
Java has moved faster in the last several release cycles than in the decade before them. A candidate whose idioms all date from Java 8 is not disqualified, but it tells you something about the codebases they have been in and how much they read outside work.
Records, and the end of the hand-written value object
A record is a shallowly immutable carrier of data with the constructor, accessors, equals, hashCode and toString derived from its components. In practice it removes an entire genre of boilerplate and an entire genre of bug, since a hand-written equals that forgets a field is one of the quieter defects in Java. They are a natural fit for DTOs, for request and response bodies, for the read models you map query results into, and for the value objects a domain model is made of. They are a poor fit for JPA entities, which need a no-argument constructor and mutability, and a candidate who knows that boundary without being told has used them properly.
Sealed types and pattern matching
Sealing a type restricts which classes may extend or implement it, which turns an open hierarchy into a closed set the compiler knows about. Combined with pattern matching in a switch, that gives you exhaustiveness: add a new subtype and every switch that must handle it stops compiling. For domain modelling this is a real gain, because the alternative is a chain of instanceof checks with a default branch that silently swallows the case somebody added last month. It is the closest Java gets to the sum types that make certain kinds of modelling comfortable, and engineers who have used them talk about the compiler catching things rather than about syntax.
Virtual threads change the shape of concurrency design
This is the change with the largest architectural consequence, so it is worth being precise. A virtual thread is scheduled by the JVM rather than the operating system, and when it blocks on I/O it unmounts from its carrier thread instead of holding it. The result is that the thread-per-request model, which the industry spent a decade abandoning because platform threads are expensive, becomes viable again at high concurrency. Straightforward blocking code that a junior can read gets throughput that previously required reactive pipelines.
That matters because reactive code is genuinely harder to write, harder to debug, and produces stack traces that tell you nothing about how you got there. If your team adopted a reactive stack purely for throughput and has been paying the readability tax ever since, this is the conversation to have. It is not a free swap, though, and the caveats are where a real candidate shows up. Pooling virtual threads defeats the point, since creating them is cheap and the pool becomes the bottleneck you removed. Code that blocks inside a synchronized block could pin its carrier thread, which is why the guidance has been to prefer an explicit lock on hot paths, and the runtime behaviour here has been improving release by release. Anything built on ThreadLocal as a per-request cache behaves differently when there are a million threads instead of two hundred. And your database connection pool is still finite, so unlimited concurrency in the application just moves the queue somewhere less visible.
Streams and Optional, and the ways they get misused
Streams are excellent for expressing a transformation and poor for expressing a process. A stream pipeline with a side effect inside a map, or four levels of nested flatMap, is harder to read and harder to debug than the loop it replaced. Parallel streams deserve particular scepticism: they use a shared pool by default, so one long or blocking task affects unrelated code, and for most collection sizes the coordination costs more than it saves.
Optional was designed as a return type to make absence explicit at an API boundary. Using it as a field, a parameter, or an entity property adds a layer of wrapping and a serialisation question in exchange for very little. And calling get without checking is just a null check with more ceremony. When we see Optional used well, it is at the edge of a repository or service where absence is a legitimate outcome the caller must handle.
The candidate who is still writing Java 8
You can spot it quickly: anonymous inner classes where a lambda would do, date handling through the old Calendar and SimpleDateFormat classes rather than the modern time API, string concatenation in loops, utility classes full of static helpers that a record and a small interface would replace. None of this is fatal. Plenty of excellent engineers work in codebases that pin them to an old version through no fault of their own. What we care about is whether they know what they are missing. Someone who says the codebase is on Java 8 and here is the upgrade I have been arguing for is a different proposition from someone who has not noticed the language moved.
Builds, Dependencies, and Why an Upgrade Is a Project
Nothing on a Java CV predicts pain like the build. This section exists because buyers routinely scope a version upgrade as a ticket, and it is one of the more expensive misunderstandings in this ecosystem.
Maven against Gradle, honestly
Maven is declarative, verbose and predictable. The build is XML, the lifecycle is fixed, and any Java engineer can open a pom file and understand what will happen. That predictability is worth a lot on a long-lived enterprise application maintained by rotating teams. The price is that anything the plugin ecosystem does not already do is awkward, and large multi-module builds are slow because the model does not do much incremental work.
Gradle is a programming model rather than a document. Incremental compilation, a build cache and a task graph make it substantially faster on large projects, and the Kotlin DSL gives you type checking and completion on the build itself. The price is that a Gradle build can become software with its own bugs, and a build script written by someone clever three years ago is a genuine maintenance liability. Our rule of thumb is boring: keep whichever one the project already uses, resist migrating for its own sake, and if you are starting fresh with a large multi-module codebase, Gradle usually wins on build times while Maven usually wins on how quickly a new team member becomes self-sufficient.
Transitive conflicts, and why the same code runs on two machines differently
Your application depends on two libraries and both depend on a third at different versions. Only one version can be on the classpath. Maven picks the one nearest the root of the dependency tree, which means an unrelated declaration order change can alter which version you get. Gradle picks the highest version it finds, which is a different kind of surprise. Either way the compiler is happy, because it compiled against whatever was there, and the failure arrives at runtime as a NoSuchMethodError or a NoClassDefFoundError on a class nobody in your team has ever imported.
The tooling exists and engineers should use it by reflex: print the resolved dependency tree, find the two paths to the conflicting library, and then decide deliberately with an exclusion or a pinned version rather than letting resolution choose. Spring Boot's managed dependency set helps enormously here by pinning a large, mutually tested set of versions, which is a real argument for aligning with it rather than declaring versions individually. Where two libraries genuinely cannot coexist, shading with relocation is the escape hatch, and it is a last resort because it makes stack traces strange and duplicates code into your artifact.
What a Java version upgrade actually involves
Almost every large upgrade in recent years has run into the same set of walls. The Java EE package rename from javax to jakarta touches import statements across the whole codebase and, more importantly, means every library you use needs a version built for the new namespace. Anything that manipulates bytecode, which includes mocking frameworks, proxy generators and some instrumentation agents, needs a release that understands the newer class file format. Internal JDK APIs that older libraries reached into are now closed off, so code that worked for a decade throws at startup unless you open the module explicitly, which is a workaround rather than a fix. Removed APIs claim a few more casualties.
None of this is hard individually. All of it is sequential, because you cannot upgrade the framework until the libraries move, and you cannot verify anything until the tests compile again. The realistic plan is staged: get the existing code compiling and running on the new runtime first while keeping the old framework, then move the framework, then adopt new language features. Each stage ends with something shippable. Anyone who quotes this as a fixed short number without having read your dependency list is guessing, and we will tell you that rather than agree with a plan we do not believe.
Reproducibility and what CI should enforce
Build the same commit twice and you should get an identical artifact. Getting there means the toolchain version is declared in the build rather than inherited from whatever the CI image happens to ship, plugin versions are pinned like any other dependency, and nothing quietly resolves a moving target over the network mid-build. On the quality side the pipeline should keep compiler warnings visible instead of drowning them, run the suite, run static analysis, and stop a merge when a dependency has a published vulnerability rather than emailing somebody a report they will archive. Candidates describe their pipeline in more or less detail, and the level of detail is the signal.
Testing: JUnit 5, Testcontainers and the Forty Second Test
Test suites in Java projects fail in a specific way. They do not disappear. They get slow, then they get flaky, then people stop trusting them, then they get skipped. Understanding why is most of what separates a useful test strategy from a coverage number.
What a unit test is, and what most Java projects call one
A unit test constructs the class under test with fakes, exercises it, and asserts on the result. It runs in single-digit milliseconds and it does not need a container, a database or a network. In a Spring codebase, an enormous number of tests labelled unit tests are nothing of the kind: they start an application context, wire real beans, hit an in-memory database and take several seconds each. Multiply that by six hundred and you have the fifty minute pipeline that nobody waits for.
The reason it happens is design rather than laziness. Business logic that lives inside a class with nine injected collaborators cannot be tested any other way. The fix is to push the logic into objects that have no framework dependency and test those directly, leaving a thin layer of wiring that genuinely needs the container. When a candidate describes that separation without being prompted, they have felt the pain and done something about it.
The Spring context test, and why it costs what it costs
Starting a full application context is expensive because it scans, instantiates and wires everything. The framework caches contexts between test classes, which is what keeps a large suite from being unbearable, and the cache key is the configuration. That detail has a sharp edge: every distinct combination of profiles, properties and replaced beans produces another context to build and hold in memory. A suite where a dozen test classes each swap out a different bean builds a dozen contexts, and the run time and memory use climb accordingly.
The practical answers are the sliced test annotations, which start only the web layer or only the persistence layer, and standardising on a small number of test configurations so the cache actually hits. An engineer who has consolidated a suite this way can usually take a serious chunk off a build time in their first fortnight, which is a good early piece of work because everybody on your team feels it immediately.
Testcontainers against the in-memory database
Testing against an in-memory database is fast and lies to you. Its dialect is not your dialect, its behaviour under concurrent access is not your database's, and every native query, upsert, JSON column, partial index and vendor-specific function either fails to run or runs differently. The bugs it lets through are exactly the ones you cannot afford: the ones about data.
Testcontainers runs your actual database in a container for the test, which removes that whole category. It is slower, and the way to live with that is to reuse a single container across the suite rather than starting one per class, and to keep the true unit tests free of it entirely so the fast feedback loop stays fast. The same approach covers a real message broker, a real cache and a real object store, and it is the single highest-value change we make to most inherited Java test suites. Wiring it well and keeping it fast in CI is usually a joint piece of work with whoever owns your pipeline, which is why DevOps engineers in India are the pairing we suggest most often on a Java engagement.
Mockito, and the tests that assert nothing
Mocking is a tool for isolating the class under test from a collaborator you own. It stops being useful when it is used to mock a library you do not control, because now your test encodes your assumption about that library rather than its behaviour, and it will keep passing after the library changes. It also stops being useful when a test verifies a sequence of internal calls, since that test now fails on every refactor while catching no bugs. A test that asserts on the output of a method survives a rewrite of the method. A test that asserts on the calls the method made does not.
Where testing gets genuinely hard is concurrency and time. Anything asserted with a sleep is a flaky test waiting for a slow CI agent. Code that calls the system clock directly cannot be tested at a boundary condition without changing the machine's date, which is why passing a clock in is one of the small design habits that pays for itself. When we look at an existing suite, the flaky tests are where we start, because a suite with three tests nobody trusts is a suite nobody trusts.
What Junior, Mid and Senior Mean on a Java Team
Java attracts long careers, so years of experience are an unusually poor guide here. Somebody can have twelve years of writing the same service layer. We assess against behaviour, and the descriptions below are worth reading as a way of deciding what level your work actually requires, since over-hiring for maintenance costs as much as under-hiring for design.
Junior
Implements a well-specified ticket inside an existing service. Follows the structure already there, writes tests when asked, uses the framework confidently at the level of annotations. Will introduce an N plus one without noticing, will call a Transactional method from within the same class, will not think about what the endpoint does when the input list has fifty thousand entries. Needs review that catches those things rather than review that comments on formatting. Genuinely productive within a bounded scope, and often the best value on a stable codebase with strong review.
Mid-level
Designs a module without supervision, chooses where the transaction boundary belongs, reads generated SQL when something is slow, knows why the pool is exhausted. Can take an unfamiliar area of a large codebase and become the person who understands it. Writes tests at the right level rather than at the level that is easiest. This is where most of our placements land, because it is the level that suits continuing work on an existing system, and it is the level most buyers actually need when they ask for a senior.
Senior
Makes the decisions the codebase lives with for years: module boundaries, what goes in shared code and what is duplicated on purpose, how errors are represented across a service boundary, what the data model does about the thing the business changed its mind on. Profiles before optimising and can explain what the collector is doing. Handles a production incident without escalating, and writes up what happened. Will argue against a rewrite, which is frequently the most valuable thing they do all year.
Specialists worth asking for by name
Past senior, depth is more useful than breadth and it is worth naming what you need. Performance work on the JVM is its own discipline, built on profiling, allocation behaviour and collector tuning, and the people who are good at it are not automatically strong at domain design. Integration and messaging is another: exactly-once semantics that are actually at-least-once with deduplication, consumer group rebalancing, schema evolution on a topic that six teams read. Batch and data work is a third, where the constraints are windows and restartability rather than latency. And Android is a separate profession that happens to share a heritage, staffed through our Android developers in India rather than from a backend pool.
How We Screen Java Engineers in India
Nothing in our process is timed, and nobody is asked to invert a tree. Java maintenance work is investigative, so the assessment is investigative. We would rather watch somebody work out what a system is doing than watch them type.
Defect hunting in a real service
Candidates receive a small Spring Boot application seeded with defects that we have all met in production: a Transactional method invoked from inside its own class, a lazy collection touched after the session has gone, a fixed pool sitting on an unbounded queue, a static map used as a cache that nothing ever evicts, and a repository call buried in a loop. They get the repository, an hour and someone to talk to. Scoring is on how they rank what they find, not on the count. Spotting three and correctly naming the unbounded queue as the one that takes the process down beats listing all five in the order they appear.
The runtime conversation
Next comes a heap dump from a service that ran out of memory and a thread dump from one that stopped responding. Open them, tell us the story. There is no way to revise for this, which is exactly why it works: a decade of real operational experience is visible inside four minutes, and so is its absence. We follow up with questions about their own systems. What did your tail latency look like before the change. What was your first theory during the last incident, and what was wrong with it.
The maintenance question
Every candidate is asked about the oldest codebase they have worked in: which part they were afraid of, and how they eventually made it safe to change. Wanting to rewrite things is normal and not a mark against anyone. What we listen for is evidence they have also done the other job, taking a module nobody would touch, wrapping it in tests that pin its current behaviour, then changing it without an incident. That is the skill most of our clients are actually buying, and very few interviews ask about it.
Written communication
When your team and the engineer share few hours, the day runs on writing. So we read things: a pull request description, a set of commit messages, an explanation of a design trade-off aimed at somebody non-technical. What we are checking is whether the reason for a change survives in text, and whether a question is phrased so it can be answered in one reply. Accent and spoken fluency get far more weight from buyers than they deserve. Clear writing gets far less.
Why this takes days rather than months
Java people are not scarce in India. What makes direct recruitment slow is the queue in front of them: sourcing, screening, three or four interview rounds, an offer, and then the weeks a candidate still owes an existing employer. A hire approved at the start of a quarter routinely produces a first commit near the end of it, and by then the outage that prompted the hire has already happened or been worked around badly.
We avoid most of that queue because our Java engineers are already here before your brief arrives, not recruited in response to it. In practice that means a shortlist in front of you within 48 hours and the person you pick starting inside 7 days. Everything described above has already happened, so your own interview is about whether they suit your system and your people rather than a second technical screen.
Four Hiring Situations We See Repeatedly
The four below are composites assembled from patterns in our inbox, not descriptions of any particular client. They appear here because the request that arrives is almost always the same phrase, we need a Java developer, while the four underlying problems need four quite different people.
Nobody left who understands the system
The two engineers who built the platform have moved on, the documentation is a wiki page last edited four years ago, and the remaining team knows how to deploy it but not how it works. Deploys have become rare, which makes each one riskier, which makes them rarer. The instinct is to hire someone to rewrite it. The better first engagement is a bounded piece of archaeology: get the build reproducible on a machine that is not anyone's laptop, get a test running against a real database in a container, turn on garbage collection and SQL logging to find out what the system actually does at runtime, and write down each module's job and its failure modes. Six weeks of that converts a frightening system into a maintainable one and gives you the information you needed before deciding anything larger. Feature work after that is ordinary.
The upgrade nobody wants to own
The application runs on a Java version and a framework version that are both past their support window, security review has flagged it, and every estimate the team produces is met with disbelief because the last one was wrong. What is needed here is not a general Java developer but somebody who has carried a real application across the namespace rename and the dependency wall, and who will sequence it so the application stays shippable at every stage rather than living on a branch for four months. Ask a candidate what they would do first. If the answer is anything other than inventory the dependencies and find out which ones have no compatible release, they have not done one.
Capacity, not cleverness
The product roadmap is agreed, the architecture is fine, the team is simply three people short and everything is late. This is the most common brief and the easiest to get wrong, because the temptation is to hire the most impressive engineer available. An architect dropped into a delivery gap will redesign things nobody asked to be redesigned. What works is a mid-level engineer who is comfortable inside an existing structure, plus the review discipline to keep quality flat as throughput rises. If the shortage spans several roles rather than Java alone, the broader picture on hiring developers in India covers how the mix is usually put together.
The system that has to survive a bigger customer
An enterprise client has signed and intends to send an order of magnitude more traffic, or a data volume that turns a report from two seconds into two minutes. Nothing is broken yet, which is the good news, and nobody knows where it will break, which is the work. This is a measurement job before it is an engineering job: load test against production-shaped data rather than an empty schema, find the queries that scale with row count rather than with page size, find the endpoints that hold a transaction open across a network call, check what the collector does when the heap is genuinely busy. The engineer you want has done this and will resist changing anything before the profile says where the problem is.
What Overlap Do You Actually Get With an India-Based Java Team?
Most offshore pages go soft here on purpose. We would rather put the numbers on the table, because for one half of the world this is a mild inconvenience and for the other half it is the single biggest constraint on the engagement, and you deserve to know which half you are in before you sign anything.
The arithmetic, in your own clock
Indian Standard Time runs five and a half hours ahead of UTC and never moves. No summer adjustment, no transition weekends, so the only thing that ever changes the gap is your own clocks going forward or back. Rather than convert everything to UTC and leave you to do the subtraction, here is a normal Indian office day, 09:30 to 18:30 IST, rewritten in the local time of the cities we most often work with.
| Your city | The Indian day, on your clock | Shared with your own 09:00 to 17:00 |
|---|---|---|
| London, winter (GMT) | 04:00 to 13:00 | 09:00 to 13:00, so 4 hours |
| London, summer (BST) | 05:00 to 14:00 | 09:00 to 14:00, so 5 hours |
| Sydney (AEST) | 14:00 to 23:00 | 14:00 to 17:00, so 3 hours |
| New York (EST) | 23:00 to 08:00 | Nothing. It ends an hour before you start |
| San Francisco (PST) | 20:00 to 05:00 | Nothing, by four hours |
The New York row is the one worth sitting with. An unshifted Indian team finishes at eight in the morning Eastern, sixty minutes before your first standup, so the shared window is not small, it is empty. San Francisco is worse by another three hours. Any vendor who implies otherwise is either quietly shifting the Indian day or counting on you not checking.
What buying overlap costs, and who pays for it
Overlap with America is not discovered, it is bought, and the currency is an Indian engineer's evening. Push the working day to 13:30 to 22:30 IST and it lands on 03:00 to 12:00 in New York in winter, which hands you back three hours between nine and noon Eastern. Pacific time is harder arithmetic and harsher: to be live for a 09:00 to 12:00 window in San Francisco, the Indian day would have to run from 22:30 to 01:30 IST. We are not going to call that a sustainable standing arrangement, because it is not one.
The middle option most US clients settle on is gentler. An Indian day finishing at 21:00 IST reaches 10:30 in the morning in New York, giving roughly ninety minutes of live time: one call, plus same-day review of whatever is urgent, without anyone working past dinner every night. Whichever pattern is chosen, it goes into a written working agreement before the first sprint instead of drifting into whatever people fall into by month three. And round-the-clock cover is never free. A real follow-the-sun rotation needs two staffed shifts, a handover document that is actually written each day, and a second engineer who knows your system well enough to act alone at three in the morning. That is a scope and a cost, agreed deliberately, not a bullet point.
Why the written record matters more on a Java codebase
A short overlap window punishes undocumented decisions, and enterprise Java carries more of those than almost any other stack. Why does this module own that table. Why is the transaction boundary drawn at the service and not the controller. Why does that retry exist and what happened the last time somebody removed it. If the answer only ever existed in a call, the next engineer guesses, and in a nine-year-old system the guesses compound. So the working rule is that arguments and their conclusions land in the pull request or the ticket, where the next person finds them in two years. Each Indian day closes with a short written handover: what merged, what is blocked, and what needs an answer from your side before Mumbai starts again. Questions arrive with the engineer's own recommendation attached, so replying takes thirty seconds rather than a calendar invitation.
Review and sprint mechanics across the gap
Standups sit inside the agreed window and stay brief, because the written notes carry the detail. Review is where quality is actually controlled, and it happens in your repository under your rules, with your engineers on anything that crosses a shared interface or changes a data model. Database migrations get a named reviewer on your side every time, since they are the change class you cannot simply revert. Demos that would land at two in the morning for somebody get recorded instead, which people watch, unlike the meeting they would have skipped.
The Parts That Go Wrong, and What Is Done About Them
There are perhaps five ways an offshore Java engagement goes wrong, and they are the same five every time. Writing them down is cheaper for everyone than you finding out in month three.
Quality drifting where you cannot see it
Nothing anybody writes on a website prevents this. Your review process does. Changes arrive as small pull requests into your repository, your engineers review anything crossing a shared boundary, and a failing pipeline blocks the merge no matter whose name is on it. Two additions are worth making specifically on Java. Assert the generated query count on the endpoints that matter, so an accidental N plus one breaks a test instead of surprising you at quarter end. And require a human on every schema migration, because that is the change you cannot roll back with a revert. Where none of that exists yet, building it becomes the opening fortnight of the engagement, since without it there is no mechanism for you to hold anyone to a standard.
Ownership of the code
The output belongs to you. How intellectual property is assigned, what confidentiality covers and how data may be handled all get written into the agreement before a line is committed, and those terms are settled between you and us rather than announced on this page. Day to day it means the work lives in your accounts and your repositories, so nothing important accumulates on a laptop you have no visibility of. Sectors with data residency or processing constraints should raise them in the first call, and you should form your own legal view of what your obligations are rather than accepting ours.
Access and security
Individual named accounts, never a shared login. Permissions limited to the repositories and environments the person needs for the work in front of them. Credentials in your secret manager, not pasted into a message thread. Everything revoked the day somebody rolls off, as a step in the offboarding rather than a memory. Production access is a decision separate from repository access, and on a great many Java engagements the offshore engineer never needs it, working from a read replica and anonymised extracts instead. We are not going to claim a certification we do not hold. What we will do is operate inside whatever controls you already run and not ask you to relax them.
When the match is wrong
Occasionally a placement simply does not fit your team, and the useful moment to say so is week two, not month four. Tell us early even if it feels blunt. How a change is handled commercially, what notice applies, how handover is paid for, is written into your signed agreement, and putting invented figures against it on a marketing page would help nobody. What we will say plainly is that a replacement Java engineer reaches you inside 48 hours of that conversation, and the choice is yours between several candidates rather than a single name presented as final. The technical half is what makes a change survivable at all, and it comes down to habit: work merged in small pieces and reviewed as it goes leaves nothing stranded, while a long branch in one person's head does not survive their departure. That is the real reason we push small merges even when a single large one would be quicker.
The costs nobody puts in the spreadsheet
Getting productive on an inherited Java application takes longer than on most stacks, because what has to be learned is not the language but a decade of decisions nobody wrote down. Two to four weeks is normal, and an undocumented build stretches it. During that window your own engineers spend real hours answering questions, and nobody ever budgets for their time. Then add the coordination tax of the time gap and any tooling you need for asynchronous work. None of that argues against hiring in India. It argues for treating the first month as ramp-up in the plan rather than assuming full velocity from the first Monday and then being disappointed.
Ways to Work With Us
One engineer inside your team
A Java engineer takes tickets from your board, joins your standup in the agreed window, and raises pull requests like anyone else on the team. It fits organisations that already have technical leadership and are simply short of hands on a system that exists. Of the three shapes here it also gives you the tightest grip, since your own review gate is the only thing between their work and your main branch.
A small squad with a lead
Three or four engineers plus somebody accountable for coordination, which is the right shape when an entire subsystem is changing hands rather than a queue of tickets. The lead writes the daily handover and is the single person your team deals with, so the meeting load on your side does not grow as the group does. Monolith splits and framework upgrades almost always end up here.
A scoped piece of work
One outcome, one end date. Stabilise a service nobody understands, halve a build time, carry an application onto a supported Java version, or instrument a system that has never had a dashboard. This is frequently the sensible way to start, because it is small enough that you can judge us on it, and whatever you decide afterwards you keep the result.
What Our Java Engineers Work With
Frequently Asked Questions
Most of our Java work is maintaining a system somebody else built. Is that what your engineers do?
Yes, and it is the majority of what we are asked for. Adding a field to a domain object that eleven classes touch, working out why a query got slow after a release, keeping a scheduled job honest at month end. We screen for people who read a codebase before proposing changes to it, because that is the skill the work actually needs.
What do you actually test for on the JVM side?
We hand over a heap dump from a service that died and ask what it says. A candidate who opens the dominator tree, finds the one retained collection holding four hundred megabytes and traces it back to a cache with no eviction policy has done this before. A candidate who suggests raising the heap size has not.
Our codebase is stuck on Java 8 and an old Spring version. Can you move it forward?
That is a project rather than a ticket, and anyone who quotes it as a week has not looked. The work is the javax to jakarta package rename, libraries that manipulate bytecode needing versions that understand newer class files, tests that relied on internal APIs now closed off, and a dependency graph that has to be resolved one conflict at a time. We plan it in stages with the application shippable after each one.
How do you tell a real Spring Boot engineer from someone who has only used the annotations?
Ask what happens when a method annotated with Transactional is called from another method inside the same class. Someone who has debugged it will tell you the proxy is bypassed and no transaction starts. Ask where the transaction ends relative to a REST call made inside it. The answers separate people who understand the machinery from people who have only decorated classes with it.
What overlap do we get with an India-based Java team?
Do the sum in your own clock. A normal Indian office day lands on 04:00 to 13:00 in London through the winter, 14:00 to 23:00 in Sydney, 23:00 to 08:00 in New York and 20:00 to 05:00 in San Francisco. So London shares four hours of it, Sydney three, and neither American coast shares any: the Indian day is over before yours opens. Overlap with the US has to be manufactured by moving the Indian working day later, and we settle that pattern with you in writing first.
Do we need a Java developer or a Kotlin developer?
For server work the two are close enough that a strong Java engineer picks up Kotlin in weeks, since both compile to the same bytecode and use the same libraries and the same Spring machinery. Android is a different question. Modern Android work is Kotlin first, with its own lifecycle, tooling and release process, so we would staff that as an Android role rather than a Java one.
Who owns the code your Java engineers write for us?
The output is yours. How intellectual property is assigned, what confidentiality covers and how data may be handled are all written into the agreement before a line is committed, and settled between us rather than declared on a web page. In daily practice the engineers commit into your own repositories under your own branch rules, so nothing of value accumulates anywhere outside your audit.
How fast can a Java engineer actually start?
A shortlist reaches you inside 48 hours, and the engineer you pick can start inside 7 days. Those numbers only hold because the people are already on our bench before your brief lands. Running the same recruitment yourself means joining a queue: sourcing, screening, several rounds of interview, an offer, then the weeks the candidate still owes an existing employer. That queue is why a hire agreed in one quarter tends to produce its first commit in the next.