Finishing Strong: Completing Your Monolith Split

In our previous posts, we discussed identifying business domains in your monolith, planning the split, and strategies for execution. Now, let’s focus on the crucial final phase: finishing the split and ensuring long-term success.

Maintaining Momentum

As you progress through your monolith splitting journey, it’s essential to keep the momentum going.

Bi-weekly Check-ins or just having a regular cadence where you track progress to completion helps co-ordinate, especially if you have many teams working on it. Use these bi-weekly meetings to:

  1. Track progress towards completion
  2. Share wins and learnings across teams
  3. Identify and address blockers quickly
  4. Maintain visibility of the project at a management level

These regular touchpoints help ensure that the split remains a priority and doesn’t get sidelined by other initiatives.

Have a Plan to Finish

One of the most critical aspects of a successful monolith split is having a clear plan to finish. Without this, you’ll start another migration before you’ve finished this one and end up with a multi-generation codebase.

Have a timeline in your bi-weekly’s, update it as you go so everyone has their eyes on teh finish line.

This timeline should:

  1. Be realistic based on your progress so far
  2. Include major milestones and dependencies
  3. Be visible to all stakeholders

Remember, if you don’t finish, you’ll start another migration before you’ve finished this one and end up with a multi-generation codebase, which will explode cognitive load and lead to escaping bugs, war rooms, and prod incidents.

Handling the Long Tail

As you approach the end of your split, you’ll likely encounter a long tail of less-frequently used features or challenging components. Keep on top of them, it’ll be hard, but worth it in the end.

Celebrate the success at the end too, mark those big milestones, it means a lot to the people that worked tirelessly on the legacy code.

Conclusion

Completing a monolith split is a significant achievement that requires persistence, strategic thinking, and a clear plan. By maintaining momentum through regular check-ins, having a solid plan to finish, and consistently measuring your progress and impact, you can successfully navigate the challenges of this complex process.

Remember, the goal isn’t just to split your monolith—it’s to improve your system’s overall health, development velocity, and maintainability. Keep this end goal in mind as you make decisions throughout the process.

As you finish your split, take time to celebrate your achievement and reflect on the learnings. These insights will be invaluable for future architectural decisions and potential migrations.

Thank you for following this series on monolith splitting. We hope these insights help you navigate your own journey from monolith to microservices. Good luck with your splitting efforts!

Strategies for Successful Monolith Splitting

In our previous post, we explored how to identify business domains in your monolith and create a plan for splitting. Now, let’s dive into the strategies for executing this plan effectively. We’ll cover modularization techniques, handling ongoing development during the transition, and measuring your progress.

If you are in the early stages of the chart, you can probably look into Modularization, if you are however towards the right hand side (like we were), you will need to take some more drastic action.

If you are on the right hand side, they your monolith is at the point you need to stop writing code there NOW.

There’s 2 things to consider:

  • for new domains, or significant new features in existing domains start them outside straight away
  • for existing domains, build a new system for each of them, and move the code out

Once your code is in a new system, you get all the benefits straight away on that code. You aren’t waiting for an entire system to migrate before you see results to your velocity. This is why we say start with the high volume change areas and domains first.

How to stop writing code there “now”? Apply the open closed principle at the system level

  1. Open for extension: Extend functionality by consuming events and calling APIs from new systems
  2. Closed for modification: Limit changes to the monolith, aim to get to the point where it’s only crucial bug fixes

This pattern encourages you to move to the new high development velocity systems.

Modularization: The First Step for those on the Left of the chart

Before fully separating your monolith into distinct services, it’s often beneficial to start with modularization within the existing system. This approach, sometimes called the “strangler fig pattern,” can be particularly effective for younger monoliths.

Modularization is a good strategy when:

  • Your monolith is relatively young and manageable
  • You want to gradually improve the system’s architecture without a complete overhaul
  • You need to maintain the existing system while preparing for future splits

However, be wary of common pitfalls in this process:

  • Avoid over-refactoring; focus on creating clear boundaries between modules
  • Ensure your modularization efforts align with your identified business domains

For ancient monoliths with extremely slow velocity, a more drastic “lift and shift” approach into a new system is recommended.

Integrating New Systems with the Monolith, for those to the Right

When new requirements come in, especially for new domains, start implementing them in new systems immediately. This approach helps prevent your monolith from growing further while you’re trying to split it.

Integrating new systems with your monolith requires these considerations:

  1. Add events for everything that happens in your monolith, especially around data or state changes
  2. Listen to these events from new systems
  3. When new systems need to call back to the monolith, use the monolith’s APIs

This event-driven approach allows for loose coupling between your old and new systems, facilitating a smoother transition.

Existing Domains: The Copy-Paste Approach for those to the Right

If your monolith is in particularly bad shape, sometimes the best approach is the simplest, build a new system then copy, paste, and call it step one from the L7 router. Don’t get bogged down trying to improve everything right away. Focus on basic linting and formatting, but avoid major refactoring or upgrades at this stage. The goal is to get the code into the new system first, then improve it incrementally.

However, this approach comes with its own set of challenges. Here are some pitfalls to watch out for:

Resist the urge to upgrade everything: A common mistake is trying to upgrade frameworks or libraries during the split. For example, one team, 20% into their split, decided to upgrade React from version 16 to 18 and move all tests from Enzyme to React Testing Library in the new system. This meant that for the remaining 80% of the code, they not only had to move it but also refactor tests and deal with breaking React changes. They ended up reverting to React 16 and keeping Enzyme until further into the migration.

Remember the sooner your code gets into the new system the sooner you get faster.

Don’t ignore critical issues: While the “just copy-paste” approach can be efficient, it’s not an excuse to ignore important issues. In one case, a team following this advice submitted a merge request that contained a privilege escalation security bug, which was fortunately caught in code review. When you encounter critical issues like security vulnerabilities, fix them immediately – don’t wait.

Balance speed with improvements: It’s okay to make some improvements as you go. Simple linting fixes that can be auto-applied by your IDE or refactoring blocking calls into proper async/await patterns are worth the effort. It’s fine to spend a few extra hours on a multi-day job to make things a bit nicer, as long as it doesn’t significantly delay your migration.

The key is to find the right balance. Move quickly, but don’t sacrifice the integrity of your system. Make improvements where they’re easy and impactful, but avoid getting sidetracked by major upgrades or refactors until the bulk of the migration is complete.

Measuring Progress and Impact: Part 1 Velocity

Your goal is to have business impact, impact comes from the velocity game to start with, so taht’s where our measurements start.

Number of MRs on new vs old systems: Initially, focus on getting as many engineers onto the new (high velocity) systems as possible, compare your number of MRs on old vs new over time and monitor the change to make sure you are having the impact here first

Overall MR growth: If the total number of MRs across all systems is growing significantly, it might indicate incorrect splitting or dragging incremental work.

Work tracking across repositories: Ask engineers to use the same JIRA ID (or equivalent) for related work across repositories in the branch name or MR Title or something, to track units of work spanning both old and new systems.

Velocity Metrics on old vs new: Don’t “assume” your new systems will always be better, compare old vs new on velocity metric and make sure you are seeing the difference.

Ok, now when you ht critical mass on the above, for us we called it at about 80%, you will need to shift, the long tail there will be less ROI on velocity, it’ll become a support game, and you need to face it differently.

Measuring Progress and Impact: Part 2 Traffic

So at this time its best to look at traffic, moving high volume traffic pages/endpoints in theory should reduce the impact if there’s an issue with the legacy system thereby reducing the support, this might not be true for your systems, you may have valuable endpoints with low traffic, so you need to work it out the best way for you.

Traffic distribution: Looking per page or per endpoint where the biggest piece of the pie is.

Low Traffic: Looking per page or per endpoint where there is low traffic, this may lead you to find features you can deprecate.

As you move functionality to new services, you may discover features in the monolith that are rarely used. Raise with product and stakeholders, ask “Whats the value this brings vs the effort to migrate and maintain it?”

  1. deprecating the page or endpoint
  2. combining functionality into other similar pages/endpoints to reduce codebase size

Remember, every line of code you don’t move is a win for your migration efforts.

Conclusion

Splitting a monolith is a complex process that requires a strategic approach tailored to your system’s current state. Whether you’re dealing with a younger, more manageable monolith or an ancient system with slow velocity, there’s a path forward.

The key is to stop adding to the monolith immediately, start new development in separate systems, and approach existing code pragmatically – sometimes a simple copy-paste is the best first step. As you progress, shift your focus from velocity metrics to traffic distribution and support impact.

Remember, the goal is to improve your system’s overall health and development speed. By thoughtfully planning your split, building new features in separate systems, and closely tracking your progress, you can successfully transition from a monolithic to a microservices architecture.

In our next and final post of this series, we’ll discuss how to finish strong, including strategies for cleaning up your codebase, maintaining momentum, and ensuring you complete the splitting process. Stay tuned!

Identifying and Planning Your Monolith Split

In the world of software development, monolithic architectures often become unwieldy as applications grow in complexity and scale. Splitting a monolith into smaller, more manageable services can improve development velocity, scalability, and maintainability. However, this process requires careful planning and execution. In this post, we’ll explore the crucial first steps in splitting your monolith: identifying business domains and creating a solid plan.

Finding Business Domains in Your Monolith

The first step in splitting a monolith is identifying the business domains within your application. Business domains are typically where “units of work” are isolated, representing distinct areas of functionality or responsibility within your system.

Splitting by business domain allows you to optimize for the majority of your units of work being in the one system. While you may never achieve 100% optimization without significant effort, focusing on business domains usually covers 80-90% of your needs.

How to Identify Business Domains

  1. Analyze Work Units: Look at the different areas of functionality in your application. What are the main features or services you provide?
  2. Examine Data Flow: Consider how data moves through your system. Are there natural boundaries where data is transformed or handed off?
  3. Review Team Structure: Often, team organization reflects business domains. How are your development teams structured?
  4. Consider User Journeys: Map out the different paths users take through your application. These often align with business domains.

For more detail here is a great book on the topic.

When to Keep Domains Together

Sometimes, you’ll find two domains that share a significant amount of code. In these cases, it might be more efficient to keep them in the same system. Consider creating a “modulith” (a modular monolith) or even maintaining a smaller monolith for these tightly coupled domains might make sense, but this is usually the exception to the rule, dont let it be an easy way out for you.

Analyzing Changes in the Monolith

Once you’ve identified potential business domains, the next step is to analyze how your monolith changes over time. This analysis helps prioritize which parts of the system to split first. Because this is where the value is, velocity, the more daily/weekly merge requests that happen in the new systems the more business impact you cause, and that’s our goal, business impact, in this case in the form of engineering velocity, don’t lose sight on this goal for some milestone driven Gantt chart.

There’s many elegant tools on the market for analysis for git and changes over time, I would encourage you to explore. We didn’t find any that worked for us because the domains were scattered throughout the code due to the age and size of our monolith (i.e. it was ancient).

What we found worked best, we used a hammer, its manual but it worked:

  1. Use MR (Merge Request) Labels: Implement a system where developers label each MR with the relevant business domain. This provides ongoing data about which domains of the system change most frequently.
  2. Add CI Checks: Include a CI step that fails if an MR doesn’t have a domain label. This ensures consistent data collection.
  3. Historical Analysis: Have your teams go through 1-2 quarters of historical MRs and label them retrospectively. This gives you an initial dataset to work with.

Once you have this data, wether it comes from the hammer approach or you find a more elegant one you want to look for patterns in your MRs. Which domains see the most frequent changes? This is how you prioritize your split.

Making a Plan

With your business domains identified and change patterns analyzed, it’s time to create a plan for splitting your monolith. Start with the domains that have the highest impact. These are the ones that change frequently.

Implement L7 Routing for incremental migration

Use Layer 7 (application layer) routing to perform A/B testing between your old monolith and new services. This allows you to:

  • Gradually shift traffic to new services
  • Compare performance and functionality potentially with AB Tests
  • Quickly roll back if issues arise

For Web Applications:

  • Consider migrating one page at a time
  • Treat each “page” as a unit of migration

Within pages sometimes we found that doing a staged approach with ajax endpoints individually helped to do the change more incrementally, but don’t let a “page” exist in multiple system for too long, it kills local dev experience, you go backwards on what you planned, you are meant to be improving dev experience, not making it worse, so finish it asap.

For Backend Services:

  • Migrate one endpoint or a small group of tightly coupled endpoints at a time
  • This allows for a gradual transition without disrupting the entire system

Also as you are incrementally migrating, if you focus is on fast killing the monolith, don’t bother deleting the old code as you go, let the thing die as a whole. This will give you more time to spend on moving to new systems. Try to not improve the experience on the old monolith, the harder it is to work on it the more likely a team is to make a decision to break something out of it, you increase the ROI this way of splitting.

Conclusion

Splitting a monolith is a significant undertaking, but with proper planning and analysis, it can lead to a more maintainable and scalable system. By identifying your business domains, analyzing change patterns, and creating a solid migration plan, you set the foundation for a successful transition from a monolithic to a microservices architecture.

In our next post, we’ll dive deeper into the strategies for executing your monolith split, including modularization techniques and how to handle ongoing development during the transition. Stay tuned!

The Evolution of Product Engineering: Adapting to a Rapidly Changing World

In today’s fast-paced digital landscape, the way we approach software development is undergoing a significant transformation. As a product engineer with decades of experience in the field, I’ve witnessed firsthand the shift from traditional methodologies to a more dynamic, customer-centric approach. This blog post, the first in our series on Product Engineering, will explore this evolution and why it’s crucial for modern businesses to adapt.

The Changing Landscape of Software Development

Remember the days when software projects followed rigid, long-term plans? When we’d spend months mapping out every detail, stake holder meetings,d esign reviews for weeks architecting a massive new system, before writing a single line of code? Well it’s becoming increasingly clear that it’s no longer sufficient in our rapidly evolving digital world.

The reality is that by the time we finish implementing software based on these detailed plans, the world has often moved on. Our assumptions become outdated, and our solutions may no longer fit the problem at hand. As Mike Tyson says, “Everyone has a plan until they get punched in the mouth.” In software development, that punch often comes in the form of changing market conditions, disruptive technologies, or shifts in user behavior.

The Pitfalls of Traditional Methods

Let’s consider a real-world example. The finance industry has been turned on its head by small, agile fintech startups. Traditional banks, confident in their market position, initially dismissed these newcomers, thinking, “They aren’t stealing our core market.” But before they knew it, these startups were nibbling away at their core business. By the time the banks started planning their response, it was often too late – they were too slow to adapt.

PayPal and Square as examples revolutionized online and mobile payments. While banks were still relying on traditional credit card systems, these startups made it easy for individuals and small businesses to accept payments digitally. By the time banks caught up, PayPal had become a household name, processing over $936 billion in payments in 2020.

Robinhood as well disrupted the investment world by offering commission-free trades and fractional shares, making investing accessible to a new generation. Established brokerages were forced to eliminate trading fees to compete, significantly impacting their revenue models.

This scenario isn’t unique to finance. Across industries, we’re seeing that the old ways of developing software – with long planning cycles and rigid roadmaps – are becoming less effective. In fact, a staggering statistic reveals that 37% of software in large corporations is rarely or never used. Think about that for a moment. We constantly hear about the scarcity of engineering talent, yet more than a third of the software we produce doesn’t provide value. Clearly, something needs to change.

The Rise of Product Engineering

Enter product engineering – a approach that’s gaining traction among the most innovative companies in the world. But what sets apart companies like Spotify, Amazon, and Airbnb? Why do they consistently build software that we love to use?

The answer lies in their approach to product development. These companies understand a fundamental truth that Steve Jobs articulated so well: “A lot of times, people don’t know what they want until you show it to them.” And as far back as Henry Ford as well said, “If I had asked people what they wanted, they would have said faster horses.”

Product engineering isn’t about blindly following customer requests or building features that someone thinks people want. It’s about deeply understanding customer problems and innovating on their behalf. It’s about creating solutions that customers might not even realize they need – yet come to love.

The Need for a New Approach

In the traditional models many companies have built, engineers are often isolated from the product side of things. They’re told to focus solely on coding, “go code, do what you are good at”, protect this precious engineering resource, and don’t let them be disturbed by non-engineering things, with the assumption that someone else will worry about whether the product actually enhances the customer’s life or gets used at all.

This leads to what I call the “feature factory” – a system where engineers are fed requirements through tools like Jira, expected to churn out code, and measured solely on their ability to deliver features to specification. The dreaded term “pixel perfect” comes to mind. But this approach misses a crucial point: the true measure of our work isn’t in the features we ship, but in the value we create for our customers and our business.

Product engineering flips this model on its head. It brings engineers into the heart of the product development process, encouraging them to think deeply about the problems they’re solving and the impact of their work. It’s about creating cross-functional teams that are empowered to make decisions, experiment, and iterate quickly based on real-world feedback.

Looking Ahead

As we dive deeper into this series on Product Engineering, we’ll explore the specific skills, mindsets, and practices that define this approach. We’ll look at how to build empowered, cross-functional teams, how to make decisions in the face of uncertainty, and how to measure success in ways that truly matter.

The evolution of product engineering isn’t just a trend – it’s a necessary adaptation to the realities of modern software development. By embracing this approach, we can create better products, reduce waste, and ultimately deliver more value to our customers and our businesses.

Stay tuned for our next post, where we’ll dive deeper into what exactly makes a product engineering team tick.

What’s your experience with traditional software development versus more modern, product-focused approaches? Share your thoughts in the comments below!