Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Consulting Services Hire Book Consulting

Microservices Architecture Services in India

Service decomposition, bounded contexts, data ownership and event-driven design, delivered by an engineering team in India for CTOs and heads of engineering in the US, UK, Canada, Australia and New Zealand. The first thing we assess is whether your monolith should stay a monolith, because for a lot of the systems we look at, it should.

See Our Cloud Architecture Work

Do You Actually Need Microservices, or Is Your Monolith Fine?

We are going to open with the case against, because that is the conversation you are least likely to get from anyone selling this work.

A monolith gives you things that are genuinely hard to replace. One deployment. One database transaction that either commits or does not. A stack trace that goes from the HTTP handler to the SQL query without crossing a network. Refactoring across module boundaries with a compiler or a linter checking your work. You give all of that up on day one of a decomposition, and you get it back only if the reason you split was real.

So what counts as real? In our experience there are three honest triggers, and none of them is about elegance.

The first is organisational. You have five teams, one release train, and every deploy needs a Slack thread and a change advisory meeting because nobody can ship without checking whether somebody else broke the build. That is a coordination cost that grows faster than headcount, and independent deployability is the only thing that fixes it. If you have one team of nine, you do not have this problem, and microservices will manufacture one for you.

The second is a genuine difference in load shape. Your image processing burns sixteen cores for four minutes and runs a hundred times a day. Your checkout API needs eighty milliseconds at the ninety ninth percentile and runs constantly. Putting those in one deployable means sizing the machine for the worst case of both and scaling on a metric that describes neither. Pulling out the outlier is usually one service, not twelve, and it delivers most of the benefit people expect from a full decomposition.

The third is failure isolation with a real cost attached. If the reporting module can consume every database connection in the pool and take the checkout down with it, and checkout downtime costs money by the minute, that is a reason. If the worst case is a slow admin screen, it is not.

Everything else on the usual list is either achievable inside a monolith or not achievable at all. Polyglot stacks are a hiring liability more often than an advantage. Reusability across services is mostly a story people tell before the second service exists. Faster onboarding is a modularity property, not a network property. Martin Fowler's argument for building the monolith first still holds up: you do not know where the boundaries are until the domain has told you, and the domain only tells you after you have shipped it a few times.

The middle path we recommend most often is a modular monolith with enforced internal boundaries. Separate schemas per module with no cross-schema joins. Module to module calls through explicit interfaces rather than direct repository access. An architecture test in the build that fails the pipeline when someone reaches across a boundary, using ArchUnit for Java, NetArchTest for .NET, or an import linter in Python. Do that for six months and two things happen. Your deploys get easier, and the boundaries that were wrong reveal themselves cheaply, in a refactor rather than a migration. When you then extract a service, you are extracting something that already stands on its own.

When we scope microservices architecture services in India, this assessment is the first deliverable, and we have written more than one report that concluded the client should not split. It is a worse quarter for us. It is a considerably better year for them.

What Goes Wrong in the Systems We Get Called Into

Almost nobody calls us at the start. They call us eighteen months in, when the architecture has stopped being a plan and started being a problem. The symptoms are consistent enough that we can usually predict the third one from the first two.

Deployments stopped being independent. There is a release checklist with an ordering constraint on it: deploy the user service first, then the billing service, then run the migration, then the notification service. That is not a set of services. That is a monolith with a network in the middle, and it now takes longer to release than the thing it replaced.

Nobody can answer a simple question about a request. A customer says their order did not appear. Four engineers open four log tools and start grepping by timestamp, because there is no correlation ID flowing across the hop from the API gateway into the queue. What used to be a single stack trace is now a scavenger hunt across six systems, and the answer arrives after the customer has already emailed twice.

One slow dependency takes down the site. A downstream service starts responding in nine seconds instead of ninety milliseconds. It never returns an error, so nothing trips. Upstream threads pile up waiting on it, connection pools fill, health checks start failing on services that are perfectly healthy, and a partial degradation becomes a full outage in about four minutes. Everyone then blames the slow service, when the actual defect was an HTTP client somewhere with no timeout configured.

Data is inconsistent and no one can say which copy is right. The customer's address is in three services. Two got the update, one did not, because the message was consumed twice and the second one wrote a stale value on top of a fresh one. There is no version on the record and no idempotency key on the handler, so the reconciliation job that someone wrote last quarter is now a permanent piece of infrastructure.

The cloud bill went up and nobody can explain the shape of it. Each service brought its own database instance, its own load balancer, its own log stream and its own staging copy. Individually every line looks small. Together it is a number the CFO now asks about monthly, and there is no per service attribution to answer with.

And the one that hurts most: onboarding a new engineer takes six weeks, because running the system locally requires eleven containers, two of which need credentials that only one person has. Velocity is now lower than it was before the migration, which is the exact opposite of the reason the migration was funded.

None of that is unrecoverable. All of it traces back to a small number of decisions taken early, usually about boundaries and data ownership, and the rest of this page is about those decisions.

How Do You Find the Right Service Boundaries?

This is the whole job. Everything else on this page is downstream of it. A system with good boundaries survives a mediocre message broker and a thin test suite. A system with bad boundaries will not be rescued by any amount of tooling, and the tooling budget is usually where teams try to fix it.

Boundaries come from language, not from nouns

The failure mode we see most often is decomposition by database table. Somebody opens the schema, sees Customer, Order, Product and Invoice, and creates four services with those names. Six months later every request touches all four, because the tables were never independent to begin with. You have turned foreign keys into HTTP calls, which is strictly worse than what you had.

Eric Evans's work on domain-driven design gives a better instrument, and the useful part is smaller than the book. Listen to how different parts of your business use the same word. In a retail company, "order" means one thing to the sales team, something else to the warehouse, and something else again to finance. Sales cares about the customer, the discount and the promise date. The warehouse cares about weight, dimensions, pick location and courier. Finance cares about tax jurisdiction, currency and revenue recognition. Those are not one Order entity with thirty nullable columns. Those are three bounded contexts with three models of the same real world thing, connected by identifiers and events.

When you cut along those seams, requests stop fanning out, because each context already holds what it needs to answer its own questions. When you cut along table names, they never stop.

Event storming, run properly

The practical way to find those seams is a facilitated session with people who actually run the business, not just engineers. You put the domain events on a wall in time order: order placed, payment authorised, stock reserved, shipment dispatched, invoice raised, payment settled. Then commands, then actors, then the policies that connect one event to the next.

What you are looking for is the places where the vocabulary changes, where a handover happens, and where an ambiguity turns out to be a genuine disagreement between two departments. Those clusters are candidate services. In a remote engagement we run this on a Miro or FigJam board across two or three sessions of ninety minutes rather than one long day, which works better across timezones anyway and gives people a night to think between sessions. The output is a context map showing each context, the relationship between them, and which direction the dependency points.

Aggregates decide your transaction boundaries

Inside a context, the aggregate is the unit that must be consistent right now. An order and its line items change together and have a rule that spans them, so they are one aggregate and one local transaction. An order and the customer's loyalty balance do not have to change in the same instant, so they are separate aggregates, and the link between them is eventual.

This is the most practical rule in the whole discipline, because it converts a philosophical question into an engineering one. One aggregate, one transaction, one owner. If your design needs two aggregates to change atomically, either they are actually one aggregate, or the business has to accept a window of inconsistency. Making that choice explicit early saves you from discovering it later inside a production incident.

Conway's Law is not a warning, it is a design input

Melvin Conway's observation from the late sixties is that a system's structure mirrors the communication structure of the organisation that built it. Most people quote it as a caution. Use it as a lever instead. If you want three independent services, you need three teams that can each own one end to end, including its database, its pipeline and its on-call. Draw an architecture that needs four teams to change one service and the architecture will lose, every time.

The Team Topologies work by Matthew Skelton and Manuel Pais is the most useful thing written on this in the last decade, and the part that matters here is the distinction between a stream aligned team that owns a slice of the product and a platform team that provides paved roads to the others. Get that wrong and you get a services layer owned by one team and a UI owned by another, which reproduces the original coordination problem in a more expensive form.

Which leads to a rule we apply bluntly. Do not create a service that no single team will own. It will be nobody's on-call, nobody's dependency upgrade, and nobody's problem until it is everybody's.

How many services is the right number

Fewer than the plan on the whiteboard. The whiteboard number is usually a list of nouns rather than a list of owners, and it shrinks a lot once you ask who is on call for each one. Start coarse and split when a specific, named pain arrives: two teams contending over the same deployment, or one part of the service needing to scale on a different axis. Splitting later is a normal refactor. Merging two services back together involves data migration, a client rewrite and an apology, and it is the direction people avoid even when they know they should take it.

The Distributed Monolith: How to Tell If You Have Built One

This is the specific failure this page exists to prevent. A distributed monolith has all the operational cost of microservices and all the coupling of the thing you left behind. It is worse than either endpoint, and it is where most failed decompositions land.

Four tests will tell you in an afternoon.

Test one, the deployment test. Pick any service. Can you ship a change to it on a Tuesday afternoon, alone, without coordinating with another team and without a documented ordering constraint? If the answer involves a release train, you do not have independent services. This is the only test that really matters, and the other three are explanations for why it failed.

Test two, the fan-out test. Trace one important user action. If loading an order detail page issues a synchronous call to seven services, your availability is now the product of eight availability figures and your latency is the sum of eight latencies including the slowest tail. Worse, you have proved the boundaries are wrong, because a service that cannot answer a question about its own domain without asking six neighbours is not holding a domain.

Test three, the data test. Run a query across your infrastructure for which services connect to which databases. If two services hold credentials to the same schema, you have a shared mutable data store with two owners and no contract. A column rename in one service will break the other in production, and no test in either repository will catch it.

Test four, the shared library test. There is a common package, usually called core or shared or platform, that every service depends on, and it contains domain entities rather than plumbing. Change it and every service needs recompiling and redeploying in a specific order. A shared library for logging setup, tracing configuration and HTTP client defaults is fine and desirable. A shared library holding the Order class is a compile-time coupling that quietly undoes the entire architecture.

The fixes are not glamorous. Duplicate the domain model per service and accept that the Order class in shipping is not the Order class in billing; that duplication is the point, not a smell. Replace synchronous read chains with data the service already holds, populated by events. Give every schema exactly one writer, and make everyone else ask through an API or read a projection. Split the shared library into a plumbing part that can stay and a domain part that has to be copied.

And one blunt piece of advice: if you fail three or four of these tests, the highest value work is often to merge some services back together before doing anything else. A well factored system of six services beats a badly factored one of twenty, and nobody ever got promoted for the second one either.

Data Ownership and Why a Shared Database Defeats the Point

One service owns a piece of data. It is the only thing that writes it. Everyone else asks, subscribes or reads a copy. That single rule carries more weight than any technology choice on this page, and it is the rule most often traded away under deadline pressure.

The argument for keeping one shared database always sounds reasonable in the room. We already have it. Joins are convenient. Two databases means two backups. The team is disciplined. But the moment a second service reads a table, that table becomes a published interface with no version, no schema contract, no consumer tests and no owner who knows who depends on it. You cannot rename a column. You cannot change a nullable to a not null. You cannot add an index without asking whose workload it affects. The database has become the coupling point, and every service change now requires a conversation about it. That is precisely the constraint you were trying to escape.

Separating the data before separating the code

The sequence that works is the opposite of what most teams try. Do the data separation first, inside the monolith, where it is cheap and reversible.

Move the tables for a candidate context into their own schema. Remove the cross-schema foreign keys and replace them with identifier references, so an order holds a customer ID rather than a database-enforced relationship. Find every join that crosses the new line and replace it with an in-process call to the owning module. That last step is where the pain lives, and it is far better to feel it in a single codebase with a compiler helping you than to feel it across a network with a JSON payload in the middle.

Once no query crosses the boundary, extraction becomes almost mechanical. Point the module at a separate database, put an HTTP or gRPC interface in front of it, and move the deployment. Teams that skip straight to extraction end up with two services sharing a schema and a plan to fix it later, and later never arrives.

The strangler fig, done with a real seam

For anything sizeable the migration is incremental. Put a facade in front of the monolith, usually at the ingress or in an API gateway, and route one path at a time to the new service while everything else continues to hit the old code. The pattern is well known. The part people get wrong is the data during the transition.

You have three options and each has a cost you should name out loud. Write to both systems from the facade, which is a dual write and will drift the moment one write fails. Write to the new system and replicate back to the old with change data capture, using Debezium against the MySQL binlog or the Postgres write-ahead log, which is our usual choice because it does not depend on application code behaving. Or keep the old system authoritative and have the new one project from its change stream, then flip authority in a planned cutover once the projection has been reconciled and running clean for a couple of weeks.

We prefer the third for anything involving money, because it keeps a single writer at all times and the cutover is one reviewable moment rather than a permanent state of hoping two systems agree.

Read models, and when they are worth it

Once data is split, some screens genuinely need a view across contexts. The dashboard that shows an order with the customer's name, the shipment status and the invoice total now spans four owners. The wrong answer is to call four services synchronously from the browser. The right answer is a read model: a service that subscribes to the events those contexts publish and maintains a denormalised view built exactly for that screen.

That view is stale by design, usually by a few hundred milliseconds and occasionally by longer when a consumer lags. Say so, and put the freshness expectation in writing before anyone builds a UI on it. If a screen genuinely cannot tolerate staleness, and some cannot, that is a strong signal the boundary is in the wrong place and the data should have been in one context to begin with.

Full CQRS with separate write and read stores is a real pattern with real value in a small number of places, mostly where read load dwarfs write load or where the two need entirely different shapes. It is also frequently adopted as a default, at which point it doubles the number of moving parts for no return. Use it where you can name the read that justifies it.

Sagas, Eventual Consistency and the Conversation the Business Has to Have

The day you split the database you lose the transaction. Placing an order used to be one commit that either happened or did not. Now it is: reserve stock in one service, authorise payment in another, create a shipment in a third, raise an invoice in a fourth. Any of those can fail after the earlier ones succeeded, and there is no ROLLBACK that reaches across all four.

Two-phase commit exists and we do not recommend it here. XA transactions across service boundaries need every participant to hold locks while waiting on a coordinator, and a coordinator failure at the wrong moment leaves those locks in place. It couples availability of the whole flow to the least available participant, which is the opposite of what you split for.

The pattern that works is the saga, which came out of database research on long-lived transactions and has aged well. A saga is a sequence of local transactions, each with a compensating action that semantically undoes it if a later step fails.

Compensation is a business decision, not a technical one

This is the part engineers cannot decide alone, and the part that most often changes the design once it is raised.

You cannot un-send an email. You can send a correction. You cannot un-charge a card in the way you can roll back a row; you issue a refund, which appears on the customer's statement as two transactions and may generate a support call. You cannot un-ship a parcel that has left the warehouse; you initiate a return, which has a cost and a policy attached to it.

So the question for the business is concrete. If payment succeeds and stock reservation then fails, do we refund immediately, hold the payment and backorder, or offer a substitute? If invoicing fails after shipment, do we ship anyway and invoice later, or hold the parcel? Those answers are policy, and they differ by company, by product line and sometimes by jurisdiction. We get them written down before an engineer designs the flow, because roughly half the time the answer reveals that two of the steps must be in the same service after all.

Orchestration or choreography

Choreography means each service listens for events and reacts. It is loosely coupled and it is genuinely hard to see. Nobody holds the flow; the flow is an emergent property of six independent handlers, and understanding it means reading six repositories. It suits short flows of two or three steps.

Orchestration means one component owns the sequence and calls each step, holding the state of the saga explicitly. You can query it, resume it, and put it on a screen for a support agent. It reintroduces a central point, which purists dislike, and in our experience the visibility is worth it for anything past three steps or anything involving money.

For orchestration we most often use a durable execution engine rather than hand-rolled state machines. Temporal, AWS Step Functions and Camunda all solve the same core problem: the workflow state survives a process restart, so a saga that was halfway through when a pod was evicted picks up where it left off instead of stranding an order. Writing that yourself means writing a persistent state machine, a timeout scheduler, a retry policy and a recovery path, and it will take longer and work less well than you think. We have seen enough hand-built saga engines to have an opinion here.

Telling the user the truth

Eventual consistency is not only a backend concern. If a customer places an order and the confirmation screen reads from a projection that has not caught up, they will refresh, see nothing, and place the order again. That is a real incident with a real duplicate charge at the end of it.

The fixes are unglamorous and they work. Return the created identifier from the write and have the UI read its own write from the writing service for the first few seconds. Show a pending state honestly instead of pretending the work is done. Use an idempotency key on the submit button so the second click cannot create a second order. Where a delay is normal, say how long it usually takes. Users tolerate a system that tells them what it is doing far better than one that appears to have lost their money.

Event-Driven Design: Outbox, Idempotency and Schema Evolution

Events are how services stay decoupled while still knowing enough to do their jobs. They are also where the subtle correctness bugs live, and the three below account for most of what we find in production reviews.

The dual write problem, and the outbox that solves it

Here is the bug in almost every first attempt at event-driven code. The service commits a row to its database, then publishes a message to the broker. Two separate systems, two separate failures. If the publish fails after the commit, the event is lost forever and no downstream service knows the order exists. If you publish first and the commit fails, you have told the world about something that never happened. Wrapping both in a try block does not fix it, because the process can die between the two lines.

The transactional outbox fixes it properly. In the same local transaction that writes your business data, insert a row into an outbox table. One commit, both facts, atomically. A separate relay then reads that table and publishes to the broker, marking rows as sent. The relay can crash and restart and the worst outcome is a duplicate publish, which your consumers already handle because of the next section.

For the relay we prefer change data capture with Debezium reading the database log directly, because it adds no load to the application and cannot be forgotten by a developer writing a new endpoint. A simple polling publisher on a short interval is a perfectly good starting point and is far easier to operate. The order of preference is: outbox with CDC, outbox with polling, then anything else. There is no fourth acceptable option, and "we will just publish after the commit" is not one.

Idempotent consumers, because delivery is at-least-once

Every mainstream broker gives you at-least-once delivery. Kafka, RabbitMQ and Amazon SQS will all hand you the same message twice under the right combination of network partition and consumer restart, and that is by design, because the alternative is losing messages. Exactly-once delivery across a network is not something you can buy; what you can build is exactly-once processing, and you build it in the consumer.

The mechanism is a deduplication table. Every event carries a stable identifier assigned by the producer, not generated at consumption. The consumer inserts that identifier into a processed-events table with a unique constraint, inside the same transaction as its business write. A duplicate hits the constraint, the transaction rolls back, nothing happens twice. That table needs a retention policy, or it becomes the largest table in your database within a year.

Where the operation is naturally idempotent, set the address to this value rather than increment the counter by one, you get this for free, and designing events that way is worth doing deliberately. Where it is not, the dedup table is the answer, and it is about thirty lines of code once.

Ordering, partitions and poison messages

Ordering is guaranteed only within a Kafka partition, so the partition key decides your ordering semantics. Key by aggregate identifier, usually the order ID or customer ID, and everything about one entity stays in sequence while different entities process in parallel. Key by something random and you will process an update before the create that it depends on. Amazon SQS standard queues do not order at all; FIFO queues do, at a throughput cost, and with a deduplication window whose current limits you should read on the AWS documentation rather than take from a page like this one.

Then there is the message that will never succeed. Bad payload, referenced entity deleted, a bug in the handler. Retried forever, it blocks the partition behind it and stops every other message for that key. Set a retry limit with exponential backoff and jitter, then move it to a dead letter queue. And here is the part teams skip: put an alert on the dead letter queue depth and give someone a runbook for draining it. We have opened dead letter queues that nobody had looked at since the day they were created, which means the business had been quietly losing work for months without anyone noticing.

Schema evolution and event contracts

An event schema is a public API with more consumers than you know about. Treat it that way from the first release.

Use a schema with a definition, Avro or Protobuf in a registry such as Confluent Schema Registry or AWS Glue, or at minimum JSON Schema checked in the pipeline. Enforce backward compatibility at build time so a producer physically cannot ship a breaking change. New fields are optional with defaults. Never remove or repurpose a field; deprecate it, wait, then remove it after you have confirmed no consumer reads it. When a genuinely breaking change is needed, publish a new version alongside the old, run both, migrate consumers one at a time, then retire the old one on a date everybody agreed to.

One design point worth arguing about: publish events that carry the state a consumer needs rather than thin notifications that force a callback. An OrderPlaced event containing the line items and the shipping address lets a consumer act on it alone. An OrderPlaced event containing only an ID means every consumer immediately calls back into the order service, and you have rebuilt synchronous coupling on top of a message bus. Fatter events cost bandwidth. Thin events cost availability. Bandwidth is cheaper.

Synchronous Call Chains and How Systems Fall Over Together

Synchronous calls are not forbidden. Sometimes you genuinely need an answer before you can respond. But every synchronous hop multiplies failure probability and adds its tail latency to yours, and chains of them are the single most common cause of the outages we get called in after.

The arithmetic nobody does

Five services in a chain, each up 99.9 percent of the time, gives a combined figure closer to 99.5 percent. On latency it is worse than the average suggests, because the slowest response in the chain dominates the ninety ninth percentile of the whole request. If each service has a p99 of 100 milliseconds, the chain's p99 is not 100 milliseconds and it is not 500 either; it is driven by the probability that at least one hop is having a bad moment, and that probability compounds with depth. Reduce depth before you optimise any individual hop.

Timeouts, budgets and the retry storm

Every network call gets an explicit timeout. Not the default, which in several popular HTTP clients is effectively infinite. A call with no timeout is a thread that never comes back, and enough of those exhausts the pool and takes down a healthy service because of a sick one.

Timeouts must also decrease down the chain. If the edge allows two seconds, and it calls a service that allows two seconds, which calls another that allows two seconds, the inner services are still working on a request the user abandoned long ago, burning capacity for nothing. Pass a deadline through the call chain, gRPC does this natively, and have each hop budget from what remains.

Retries are where a partial problem becomes a total one. A struggling service returns errors, three upstream services each retry three times, and it is now receiving four times its normal traffic at the worst possible moment. Retry only idempotent operations. Cap attempts low, usually two. Use exponential backoff with jitter so callers do not synchronise into a thundering herd. And use a retry budget that stops retrying entirely once the error rate crosses a threshold, because past that point you are the outage rather than the victim of it.

Circuit breakers, bulkheads and shedding load

A circuit breaker watches the failure rate to one dependency and, past a threshold, stops calling it and fails fast instead. This does two things: it stops your threads piling up on a dead dependency, and it gives that dependency room to recover instead of being hammered while it restarts. After a cooldown it lets a small number of probe requests through and closes again if they succeed. Resilience4j on the JVM, Polly on .NET, or the equivalent behaviour in an Envoy sidecar if you are running a mesh.

Bulkheads mean separate connection pools and thread pools per dependency, so a slow recommendations API cannot consume the capacity your checkout path needs. It is the same principle as compartments in a ship, and it is the difference between a degraded feature and a dead site.

Then there is what you do when you are the one being overloaded. Shed load deliberately at the edge, rejecting requests quickly with a clear status rather than accepting everything and timing out, because a fast rejection lets a client back off while a slow timeout ties up resources on both sides. Rank your endpoints by business value in advance so that when you shed, checkout survives and the recommendation carousel does not. Decide that in a design session, not at 2am.

The fallback that is often better than any of this

Ask whether the call needs to be synchronous at all. Does the order service really need to call the loyalty service before responding, or can it publish an event and let loyalty catch up in a hundred milliseconds? Can this service hold its own copy of the small amount of reference data it keeps asking for, kept fresh by events? Most of the deep call chains we untangle were never required by the domain. They exist because HTTP was the first tool everyone reached for, and nobody revisited it once the shape of the system changed.

Distributed Tracing Is Not Negotiable

In a monolith, debugging is a stack trace. In a distributed system, the equivalent artefact does not exist unless you build it, and a team that splits its services without instrumenting them has taken away its own ability to answer questions. We treat tracing as part of the definition of done for the first service, not a phase two item, because retrofitting it across twelve services later is several times the work.

OpenTelemetry is the answer here and there is no serious competitor. It is vendor neutral, it is a CNCF project, and it means your instrumentation is not a bet on a monitoring vendor you may want to leave. Auto-instrumentation covers the HTTP and database layers in most languages with configuration rather than code, and you add manual spans where your own domain logic needs to be visible.

Trace context propagates through the W3C traceparent header, which is a published standard and matters because it means a Java service, a Go service and a Node service will agree on the identifiers without custom glue. The part teams get wrong is asynchronous boundaries. A trace that dies when a message hits the queue is worth much less than one that continues into the consumer, so put the trace context into the message headers on publish and extract it on consume. Do that on day one and the outbox relay carries it along too.

Three things make the difference between traces that get used and traces that get ignored. First, put the trace ID in every log line, so a support ticket goes straight from an error message to the full request path. Second, sample intelligently: head sampling at a low percentage plus a rule that keeps one hundred percent of errors and slow requests, or tail sampling if your collector supports it and your volume justifies it. Sampling everything at full rate on a busy system produces a bill nobody wants to defend. Third, propagate a small number of business identifiers as span attributes, the order ID and tenant ID for instance, so an engineer can find the trace for a specific customer complaint rather than a representative one.

On the backend, Jaeger, Grafana Tempo, AWS X-Ray and the commercial platforms all work. The choice matters far less than the instrumentation, and with OpenTelemetry in place you can move between them without touching application code. What we measure at the end of an engagement is simple: can an engineer go from a customer complaint to the failing hop in under ten minutes using only what is on the screen? If not, the observability work is not finished, whatever the dashboard count says.

Testing Strategy: Contract Tests Beat End-to-End Suites

Every team that splits a monolith tries to keep testing the way they always did, by standing up the whole system and driving it. It works for about four services. By twelve it takes forty minutes, fails randomly twice a day, and the team has learned to re-run it rather than read it. A test suite nobody trusts is worse than no suite, because it costs time and provides false comfort.

Consumer-driven contract tests

This is the highest value testing you can add to a distributed system, and it is the least commonly present when we arrive.

The consumer writes a test describing exactly what it needs from the provider: this request produces this response shape with these fields. That expectation is published as a contract, to a Pact Broker or PactFlow if you are using Pact, or through Spring Cloud Contract if you are on the Spring stack. The provider's own pipeline then replays every consumer contract against a real running instance of itself. If the provider changes a field that a consumer depends on, the provider's build fails, in seconds, in the repository where the change was made and by the person who made it.

That last detail is the whole point. The failure lands where the decision was taken, not three days later in a shared environment where two teams argue about who broke it. It also gives you something more valuable than the tests: a live, accurate record of who actually depends on what, which is information nobody in a growing system otherwise has. Pact's can-i-deploy check turns that record into a deployment gate.

Component tests with real infrastructure

Below contracts, test each service on its own but against real dependencies where it matters. Testcontainers starts a genuine PostgreSQL, a genuine Kafka and a genuine Redis in Docker for the duration of the test run, which means your SQL is tested against the engine you deploy on rather than against an in-memory database with different semantics. We have lost enough hours to H2 accepting queries that Postgres rejects to be firm about this.

Outbound collaborators get stubbed with WireMock or the equivalent, driven by the same contracts, so the stub cannot drift from reality. Run these on every commit. They are the workhorse layer and they should be the bulk of your suite.

The few end-to-end tests worth keeping

Keep a small number, five to fifteen, covering only the journeys where failure costs money: sign up, checkout, payment, the core action of your product. Run them against a real deployed environment rather than a local composition. Accept that they are slow and treat any flake as a defect to be fixed that week rather than a fact of life.

Then push the rest of your confidence into production. Health checks that verify dependencies rather than returning a static OK. Synthetic transactions running the critical path every few minutes from outside your network. Canary releases where a small share of traffic sees the new version while error rate and latency are compared automatically, with an automatic rollback if they degrade. In a system of any size, this catches more real problems than any pre-production suite, because it tests the thing you actually shipped with the traffic you actually get.

Versioning and Releasing Services Independently

Independent deployability is the benefit you paid for. If you cannot release one service without releasing another, everything above was a lot of effort for a slower monolith. Protecting it is mostly about how you change interfaces.

The default rule is that you do not break compatibility, you add. Add fields, never remove or rename them. Make new fields optional with sensible defaults. Tolerate unknown fields on the consumer side so a producer adding something does not break anyone. This is the tolerant reader idea and it is the cheapest resilience in the whole architecture.

When a change genuinely cannot be additive, run both versions. The expand and contract sequence is worth learning as a discipline: add the new field or endpoint alongside the old, write to both while reading from the old, migrate consumers one by one on their own schedule, switch reads to the new, then remove the old once you can prove nothing calls it. Every step is independently deployable and independently reversible, which is exactly what you want at 4pm on a Thursday.

"Prove nothing calls it" needs actual data, not memory. Log usage of the deprecated path with the caller identity attached, watch it for a full business cycle so you catch the monthly batch job, and only then delete. Contract records from your broker give you the same answer for consumers that run tests. Deleting an endpoint that a quarterly reconciliation job depends on is a classic, and it surfaces in the worst week of the quarter.

On URI versioning, keep it out of the path if you can. A /v2/ prefix encourages a big-bang migration and a permanent maintenance burden, because /v1/ never goes away. Additive evolution with a deprecation policy handles most change. Where you do need explicit versions, media type negotiation keeps the resource identity stable, and a v2 path is acceptable for a genuine redesign of a public API with external consumers you cannot coordinate with.

Database migrations follow the same rule and get it wrong more often. A migration that drops a column deploys before or after the code that stopped using it, and in a rolling deployment both versions of the code are running at once. So: add the new column, deploy code that writes both and reads the old, backfill, deploy code that reads the new, then drop the old column in a later release. Four deployments instead of one, and no downtime and no rollback that leaves you stranded. Tools like Flyway and Liquibase manage the ordering; they do not make an unsafe migration safe.

Underneath all of this sits per-service pipelines and per-service versioning. One repository per service or a monorepo with independent build targets, either works. What does not work is a shared version number across services, because the moment you have a release 4.2 that means something across the estate, you have a release train again and you are back where you started.

What Does Each Service Actually Cost You to Run?

This is the section that gets left out of most architecture proposals, and it is the one that determines whether the programme is still liked in eighteen months. Every service has a standing cost that is not the compute bill, and it is paid every month whether the service changes or not.

A service needs a repository, a pipeline, a container image and a registry entry. It needs a deployment definition and probably a Helm chart. It needs its own environments, and if you have development, staging and production, that is three of everything. It needs a database, or a schema on a shared instance, and that database needs backups, restore testing, encryption, and a plan for how it gets patched. It needs an ingress or a service entry, a certificate, a DNS record. It needs dashboards, alerts, a log stream and a retention policy. It needs an on-call owner and a runbook. It needs its dependencies upgraded when a CVE lands, which is not on your roadmap and does not care about your roadmap. It needs an access policy, a secret store entry and a rotation schedule. It needs documentation someone will actually read.

Multiply that by the number of services in the plan. Two of those are trivial. Thirty are a full-time platform capability that has to exist before the thirtieth service does, which is why we push so hard on starting coarse.

The infrastructure line is real too and it does not scale down the way people assume. A monolith on three application servers might become twenty deployments each with a minimum of two replicas for availability. Idle capacity is now spread across forty pods instead of three processes, and the sum of the minimums is larger than the peak of the thing it replaced. Add a load balancer per service, a database per service, and the cross-availability-zone data transfer charges that appear the moment chatty services land in different zones. That last one surprises people every time, and it shows up as a line nobody budgeted.

There are honest ways to bring it down. Share a database instance with separate schemas and separate credentials, which keeps logical ownership while paying for one machine. Bin-pack services onto shared compute rather than giving each its own node. Scale non-production to zero at night, since a staging environment running at 3am on Sunday is pure waste. Attribute cost per service from day one with tags and a tool like OpenCost or your cloud's native cost allocation, because a team that can see its own number behaves differently from one that cannot.

The economics are also why the platform layer has to come before service number five, not after service number twenty. A service template that generates the repository, pipeline, deployment manifest, dashboards and alerts from one command turns the standing cost into something amortised. Without it, every service is hand-built from scratch and the tenth one takes as long as the first.

We put all of this in the assessment as a written estimate of ongoing operational load, expressed in engineer time and infrastructure line items, so the decision to split is taken with the running cost visible rather than discovered later.

What a Microservices Architecture Engagement Covers

No engagement includes all of this, and the sequence matters more than the list. Building a service mesh before you have data ownership sorted is an expensive way to make a coupled system harder to read. Here is the surface area, roughly in the order we work through it.

Architecture assessment

Typically two to three weeks. We read the codebase, the schema, the deployment configuration and the incident history from the last six months, then interview the engineers and at least two people from the business side. Static analysis of module dependencies, a map of which code touches which tables, and a review of the call graph from your traces if they exist. What comes back is a written assessment: the current boundaries whether or not anyone designed them, where the coupling actually is, an honest recommendation on whether to split, and a sequenced plan if the answer is yes. That document is yours regardless of what you do next.

Domain modelling and boundary design

Facilitated event storming sessions with your domain experts, a context map, aggregate definitions per context, and a decision on which contexts become services now and which stay inside the monolith for the moment. Recorded as architecture decision records in your repository so the reasoning outlives the people who were in the room.

Decomposition roadmap

The order of extraction, chosen by value and risk rather than by ease. Usually the first extraction is something with a clear seam and a real pain attached, so the team learns the mechanics on work that pays for itself. Each step has a rollback path and a measurable outcome.

Data separation

Schema splitting, foreign key removal, join elimination, ownership assignment per table, and the change data capture pipeline if the migration needs one. This is the longest part of most engagements and the part that determines whether the rest works.

Service implementation

The extracted services themselves, with their APIs defined in OpenAPI or Protobuf, contract tests published, health and readiness endpoints that check what they claim to check, structured logging with correlation IDs, and OpenTelemetry instrumentation from the first commit.

Messaging and event infrastructure

Broker selection with the reasoning written down, topic and queue design, the transactional outbox implementation, consumer idempotency, dead letter handling with alerting, and the schema registry with compatibility rules enforced in the pipeline.

Resilience engineering

Timeout and deadline propagation, retry policies with budgets, circuit breakers, bulkhead isolation, graceful degradation paths, and a failure injection exercise in a non-production environment so that the first time you see the behaviour is not during an incident.

Observability

Distributed tracing end to end including across asynchronous hops, service level indicators that reflect user experience rather than CPU, alerts tied to those indicators, and dashboards built to answer specific questions rather than to display everything available.

Platform foundations

A service template that produces a working, instrumented, deployable service from one command, along with pipeline templates and a documented paved road. This is what stops service number ten costing as much as service number one.

Handover

Architecture decision records, runbooks that name the first three checks per alert, a documented context map kept current, and working sessions where your engineers extract a service themselves with ours reviewing rather than driving. If your team cannot do the next extraction without us, the handover is not finished.

Three Situations We Get Called Into

These are composite situations drawn from patterns we see repeatedly, not accounts of specific client projects. The shapes are real even where the details are illustrative.

The B2B platform where one release train blocks five teams

A subscription platform, seven years old, one Django or Rails or Spring codebase depending on the year it started. Five product teams, one deploy every second Tuesday, a release checklist with fourteen manual steps and a rollback that has been used twice and worked once. Nobody wants a rewrite. They want to ship on their own schedule.

What we do is not a decomposition. It is a modular monolith first: separate schemas per domain, an architecture test in the build that fails on a cross-boundary import, and interfaces between modules. That takes a few months and it delivers most of the coordination relief on its own, because merge conflicts and blast radius shrink immediately. Then one module gets extracted, usually the one with the clearest ownership and the loudest pain, and the team learns the operational mechanics on a service where a mistake is recoverable. The measure of success is deployment frequency per team and change failure rate, not the number of services.

The retailer whose checkout falls over when the catalogue is slow

Twelve services already, built over two years by people who have mostly moved on. Checkout calls pricing, pricing calls inventory, inventory calls a supplier integration, and the supplier's API is slow on Monday mornings. Nothing errors. Everything just gets slower until the connection pools fill and the site stops.

The first fix is not architectural, it is a day of work: explicit timeouts on every client, deadline propagation from the edge, circuit breakers around the supplier integration, and a bulkhead so the supplier calls cannot exhaust the pool that checkout needs. That stops the bleeding within a sprint. The architectural fix follows, and it is to remove the chain rather than harden it. Pricing holds its own copy of the inventory signals it needs, updated by events, so checkout no longer reaches four services deep to answer a question about a product. Availability goes up because the dependency count went down, which is a more reliable lever than any resilience library.

The health-tech company that has to split for data residency

A platform serving clients in more than one jurisdiction, where certain categories of personal data must be processed and stored within a specific region. The current monolith has one database in one region and no way to segment. The driver here is legal, not technical, and that changes the design conversation entirely.

The boundary is drawn around the regulated data first, not around the domain. A dedicated context owns that data, deployed in-region, with everything else holding only pseudonymous identifiers and referencing back. Access is logged at the service boundary because the audit trail is a deliverable, not a nice-to-have. Events crossing the boundary carry references rather than payloads, which is the opposite of our usual advice about fat events and is correct here for exactly that reason. We design and build to the requirement as your counsel and your compliance function state it; the interpretation of what a regulation requires is theirs to give and yours to confirm with them, not ours to assert.

How We Run Microservices Delivery from India

Architecture work is high-context work. It depends on conversations with people who know why a decision was taken in 2019. That makes the delivery model more important here than on a well-specified build, so here is how it actually runs rather than how it sounds best.

The overlap window, honestly

Our engineers work Indian Standard Time, which is five and a half hours ahead of UTC. On a standard 09:30 to 18:30 IST day, that gives a UK client roughly four hours of live overlap in your morning and early afternoon, which is comfortable and needs no special arrangement.

US Eastern is harder and we are not going to pretend otherwise. A standard Indian day finishes at about 08:00 in New York. To get overlap we shift the Indian day later, typically to something like 12:30 to 21:30 IST, which produces one to two hours with your morning. US Pacific effectively has no natural overlap at all; getting it requires a genuine shift arrangement for named engineers, and that is a real cost in fatigue and retention which we would rather discuss openly than absorb quietly and have you notice later in the turnover.

Australia and New Zealand run the other way. Sydney is four and a half to five and a half hours ahead of India depending on daylight saving, so your afternoon is our morning and a small shift earlier on our side gives three to four hours. New Zealand is tighter and usually needs our team starting early rather than yours staying late.

The window is agreed with you before the engagement starts and it goes in the engagement documents. We would rather commit to four good hours than claim a coverage model that quietly depends on people answering messages at midnight.

Written first, because the meeting will not include everyone

Distributed teams across a ten hour gap cannot run on meetings, and architecture decisions made verbally in a call half the people missed will be re-litigated for months. So every significant decision becomes an architecture decision record in your repository: the context, the options considered, the choice, and the consequences we accept. It is reviewed as a pull request like any other change.

Daily written standup posted in your channel before your day starts, covering what moved, what is blocked and what needs a decision from you. A short live call inside the overlap window for the things that genuinely need a conversation. Long-form design documents circulated for asynchronous comment ahead of any session, so the meeting is for resolving disagreement rather than for reading.

Code review and the definition of done

Every change goes through pull request review, and for architecturally significant changes at least one reviewer is from your side. That is not a formality; it is how context transfers, and it is the mechanism that stops you waking up in a year with a system you cannot maintain.

A change is done when it is reviewed and merged, covered by unit and component tests, covered by contract tests where it crosses a service boundary, instrumented with tracing and metrics, documented where behaviour changed, deployed to staging and verified, and behind a feature flag if it needs a controlled rollout. Not one of those is optional, and the list is agreed with you at the start rather than negotiated per pull request.

Vetting, communication and how the team is picked

Distributed systems work exposes weak engineers faster than most disciplines, because the failure modes are subtle and the debugging is unforgiving. Our technical assessment for this work is practical rather than a puzzle round: a design exercise on a real decomposition problem, a code review exercise where the candidate has to find the concurrency bug and explain it, and a discussion of a system they have actually operated including what went wrong with it.

English is assessed in the same conversation rather than separately, because the thing that matters is whether someone can disagree with your architect clearly and in public. You interview the engineers proposed for your team before they start, and you can say no.

Security and access

Engineers work under named identities in your identity provider, with access scoped to what the work requires and revoked when it ends. Source control, cloud consoles and observability tooling are accessed through your single sign-on and your multi-factor policy, so your existing audit trail covers our activity the same way it covers your own staff. Where production data would otherwise be needed for debugging, we work from anonymised or synthetic datasets by default. The specific arrangements for confidentiality, intellectual property assignment and data handling are set out in the engagement documents before anyone gets access, and we would encourage you to have your own counsel review them rather than take a web page's word for it.

Risks We Will Name Before You Sign Anything

Decomposition programmes fail in predictable ways. Here are the ones we watch for, including the ones that are our problem rather than yours.

The migration never finishes. A strangler migration that stalls halfway leaves you running two systems, two deployment models and two mental models at once, which is more expensive than either. The mitigation is to sequence by value so that each step pays for itself and the programme survives a change of priorities, and to define what "done" means for each extraction before starting it. If a step cannot state its own finish line, it is not ready to start.

The boundaries turn out to be wrong. This is not a failure, it is normal, and the risk is only in how expensive the correction is. Splitting later is cheap. Merging back is not. That asymmetry is the whole reason we start with fewer, larger services and treat the first six months as evidence gathering rather than as a finished design.

The knowledge lives in our heads instead of yours. This is the real risk of any offshore engagement and it is on us to prevent. The mitigations are structural: your engineers review our pull requests, decisions are written down as ADRs rather than explained in calls, and the last phase of every engagement is your team doing an extraction with us reviewing rather than the other way round.

The timezone gap slows decisions rather than the work. Engineering across a ten hour gap is fine. Waiting eighteen hours for an answer to a blocking question is not, and it is usually the actual cause when a distributed team feels slow. We handle it by naming a decision owner on your side, agreeing which classes of decision the team can make alone, and raising anything blocking before your day ends rather than after ours does.

Operational maturity does not arrive with the architecture. Microservices assume a level of deployment automation, monitoring and on-call discipline that many organisations do not have when they start. Splitting a system that is deployed manually will produce a slower manual deployment multiplied by the service count. We assess this up front and will say plainly if the pipeline work has to come first.

Team continuity. Attrition exists in this industry everywhere, and anyone claiming otherwise is selling. What we can talk about concretely is how the engagement is structured to survive it: overlapping knowledge across at least two engineers per area, documentation as a deliverable rather than an afterthought, and your own people in the review path from day one. The specific commercial arrangements around team changes, notice and handover are set out in the engagement documents for your situation rather than promised on a web page.

Engagement Models

Three shapes, and the right one depends mostly on whether you have an architecture capability of your own and whether the work has an end.

Architecture assessment

A fixed piece of work with a written deliverable at the end: current state, coupling analysis, boundary recommendations, a sequenced decomposition plan and an honest recommendation on whether to proceed. Two to three weeks. Some clients take that document and execute it themselves, which is a perfectly good outcome and one we design the document to support.

Scoped decomposition project

A defined outcome with a defined end. Extract three services, separate the data, stand up the messaging infrastructure, deliver the platform template. Agreed scope, agreed deliverables and a handover at the end. This suits organisations with their own engineering team who need specific work done without pulling people off the product roadmap.

Dedicated team

Engineers who work only on your system, in your repositories, in your standups, as an extension of your team. This is the model for a multi-quarter migration where the scope will legitimately change as you learn. You direct the work. The overlap window, the review process and the reporting cadence are agreed before the first sprint.

Commercial terms, coverage hours, intellectual property assignment and notice arrangements are set out in the engagement documents before work starts. We put the specifics in writing for your situation rather than publishing numbers that would not apply to it.

Where This Sits Alongside Our Other Work

Decomposition rarely arrives alone. Once you have services, something has to run them, and cluster design, autoscaling and workload orchestration sit with our Kubernetes services team. The two engagements are frequently staffed together, and the order matters: boundaries first, orchestrator second. If the wider question is which cloud, how the accounts and networks are structured and what the platform looks like underneath everything, that is cloud architecture, and it is a different conversation from this one.

Where the problem is connecting systems that already exist rather than splitting one that does, third-party connectors, gateways and data flows between platforms, our API integration work covers that. For staffing rather than a scoped engagement, you can hire Java developers in India for JVM-based service work and Spring ecosystem migrations, or hire Go developers in India where the services are small, network-bound and you want a low memory footprint per instance.

Frequently Asked Questions About Microservices Architecture in India

Should we break up our monolith at all?

Often not. If one team owns the whole codebase, deploys weekly without stepping on each other, and nothing in the system needs to scale on a different curve from everything else, splitting it will cost you speed and buy you very little. The honest trigger is organisational: several teams blocked behind one release train, or one component whose load profile has nothing in common with the rest. Absent those, a well modularised monolith is the cheaper system.

How many services should we end up with?

Fewer than you are imagining. Count your teams, not your nouns. A service that no single team owns end to end will rot, so the practical ceiling is roughly the number of teams you have plus a small number of genuinely shared platform services. The plan that comes to us is almost always larger than the plan we leave with, and the smaller one ships sooner. You can always split a service later. Merging two back together is the painful direction.

Can our services share one database if we are careful?

Careful does not survive contact with a deadline. The moment two services read the same table, that table becomes a public API with no versioning, no owner and no test coverage, and a schema change in one service breaks the other at runtime. Sharing a physical database server is fine when budget demands it. Sharing schemas is not. If you cannot separate the data, you have not found a real boundary yet, and the split should wait.

What is a distributed monolith and how do we know if we have one?

It is a system with the deployment complexity of microservices and the coupling of a monolith. Three tests find it quickly. Can you deploy any one service on a Tuesday without coordinating a release with another team? Does a single user action fan out into a chain of five or more synchronous calls? Do two services write to the same tables? Fail any of those and you have paid the network tax without collecting the independence.

How do you handle a transaction that spans three services?

With a saga, which is a sequence of local transactions where each step has a compensating action if a later step fails. There is no distributed rollback, so the compensation is a business decision rather than a technical one. Refunding a payment, releasing reserved stock and emailing the customer are things the business has to agree to, in that order, before an engineer writes any of it. We run that conversation early because it usually changes the design.

Do we need Kafka, or will a simpler queue do?

Most teams need a queue, not a log. If consumers only need each message once and nobody replays history, Amazon SQS, RabbitMQ or Google Pub/Sub will do the job with a fraction of the operational weight. Kafka earns its cost when several independent consumers read the same stream at different speeds, when replaying a topic from the beginning is a real recovery path, or when ordered partitions matter to correctness.

How do you test microservices without a giant end-to-end suite?

By pushing the work down. Consumer driven contract tests with Pact or Spring Cloud Contract catch the integration break at the point where it happens, in each service pipeline, in seconds. Component tests with Testcontainers run one service against a real database and stubbed collaborators. End to end tests survive only for the handful of revenue critical journeys, because a suite that spans twelve services is slow, flaky and trusted by nobody after a month.

How does an India based team work on our architecture across timezones?

With a written first culture and an agreed overlap window. A UK client gets roughly four hours of live overlap on a standard Indian day. US Eastern gets one to two hours once the Indian day is shifted later, and US Pacific needs a genuine shift arrangement rather than goodwill. Architecture decisions are recorded as ADRs in your repository so the decision survives the meeting nobody could attend. The window and the cadence are agreed with you before work starts.

Send Us Your Dependency Graph

Tell us the shape of the system: how many services or modules, which database, which teams own what, and the last thing that broke because two parts of it were tied together. We will come back with where the real boundaries are, what we would extract first, and a straight answer on whether you should be splitting at all.