How We Are Migrating Monolith To Microservices On AWS

Our legacy application is a monolith hosting on multiple EC2 machines on AWS. We are currently under the process of migrating it to microservices architecture mainly due to the following reasons:

  • It is not modular and things are tightly coupled so teams are stepping on each other’s toes while developing on it
  • The technology is outdated and we don’t have the expertise on continuous maintenance and development
  • The application is poorly tested in general therefore we don’t feel safe to change the system. One simple change could result in bugs in other places we are not aware of.
  • Various performance issues

The migration comes with the following challenges:

  • We still need to keep the monolith running and serving our customers.
  • We can’t afford a complete rewrite of the monolith as that would take ages.

Our team decided to follow Strangler Fig Pattern and gradually tear the monolith apart.

Here is how the entire migration process looks like:

Monolith Migration

In order for this to work we need to identify our bounded contexts/domains following domain-driven design. One useful way for this process would be Event Storming. The In this case Domain A would be represented in the diagram as Frontend Application A, Backend Microservice A and Database A, these will run inside ECS or EKS behind a Service Mesh.

We take what we need from the monolith by streaming change log from RDS read replica via AWS Data Migration Service to AWS MSK topic, which will give us Kafka events which look like the following:

{
  "topic": "<topic_name>",
  "partition": 3,
  "offset": 53870799,
  "tstype": "create",
  "ts": 1651268333902,
  "broker": 2,
  "key": "<partition_key>",
  "payload": "<the_actual_payload>",
  "_datetime": "2022-04-29T21:38:53Z",
  "_payload": {
    "data": {
      "id": 1,
      "createdOn": "2021-01-23T20:38:16Z",
      "modifiedOn": "2022-04-29T15:45:04Z",
      "version": 0,
      "firstName": "Foo 2",
      "lastName": "Bar 2"
    },
    "before": {
      "id": "1",
      "firstName": "Foo",
      "lastName": "Bar"
    },
    "metadata": {
      "timestamp": "2022-04-29T21:38:53.752816Z",
      "record-type": "data",
      "operation": "update",
      "partition-key-type": "primary-key",
      "schema-name": "<schema_name>",
      "table-name": "<table_name>",
      "transaction-id": 569130413019234
    }
  }
}

Since we are already on AWS we try to its services available. If you are not on AWS, then AWS Data Migration Service could be replaced with Debezium.

Then we create a CDC service transforming raw change event into domain event, this is acting as our ACL (Anti Corruption Layer).

From now on inside each microservice we have Kafka consumers to pick up the events and persist it into individual databases.

On Application Load Balance layer we will forward requests to either old monolith or new microservice frontend based on routing rules. If we are happy with the new microservice then we will deprecate things inside the monolith and make it smaller. Repeating the process would eventually allow us to split the monolith.

During the process monolith application would likely need the data from the new microservices, we can either follow the same approach to feed the data back. How kafka consumer interacts with monolith would depend on individual cases.

There will a lot of issues to resolve along the way but hopefully this will get us where we want to be in future.

Here are some important things to bear in mind during the migration process:

  • Think about whether what you need is just modular monolith instead of microservices architecture.
  • Keep minimal changes to the monolith.
  • Start with the most independent domain.
  • Follow Kafka best practices, e.g. make sure Kafka consumers are idempotent.
  • Have faith that all these work will eventually be paid off as it will be a long journey.