Back to Blog

From 15 Repositories to 1: Consolidating a Multi-Vendor Python Codebase

M
Mautomic Team
9 min read

Fifteen repositories. Fifteen nearly identical codebases. One bug fix that had to be applied fifteen times.

That was the reality of our client's integration layer - a set of Python services that connected their platform with fifteen different vendor systems. Each vendor had its own repository, its own CI pipeline, its own deployment configuration, and its own copy of what was, in practice, the same code with minor variations.

The code across repositories was 90% identical. The maintenance burden was 100% painful. This is the story of how we consolidated fifteen repositories into one, what drove the urgency, and what we learned along the way.

The Starting Point: One Repo Per Vendor

Our client operates a platform that integrates with multiple external vendor systems - think of them as independent service providers, each with their own API, their own data formats, and their own quirks. The platform needs to exchange data with all of them: sending welcome messages, processing monthly reports, handling registrations, running renewals, and delivering recommendations.

The original architecture was simple and sensible at small scale: each vendor got its own repository. The repository contained a set of Python services - Welcome, Monthly, Register & Verify, Auto Renew, Recommends - each deployed as an AWS Lambda function via Serverless Framework.

At three vendors, this worked fine. At five, it was getting tedious. At fifteen, it was actively harmful.

The Pain: Death by a Thousand Copy-Pastes

Here's what maintaining fifteen near-identical repositories actually looks like:

Bug fixes multiplied by fifteen. Discover a bug in the Monthly service's date parsing logic? Great. Now open the same file in fourteen other repositories, apply the same fix, create fourteen pull requests, get fourteen code reviews, and trigger fourteen CI/CD pipelines. Miss one? That vendor's Monthly service has a different bug than all the others, and you'll find out in production.

Features deployed fifteen times. Build a new retry mechanism for API failures? Beautiful. Now copy it into fourteen other codebases. But wait - repo #7 has a slightly different error handling approach because someone made a local fix six months ago. And repo #12 has a different version of the HTTP client library. Now you're not just copying code, you're debugging subtle differences between "identical" codebases.

Dependency drift. When fifteen repositories evolve independently, their dependencies diverge. Repo #1 might use requests==2.28.0, repo #8 might use requests==2.31.0, and repo #14 might pin urllib3 to a version that conflicts with the newer requests. Upgrading a common library means navigating fifteen different dependency resolution scenarios.

Onboarding friction. A new developer joins the team. "Which repository should I look at?" "Well, they're all basically the same, but each one has small differences." Now the new developer has to mentally track which patterns are universal and which are vendor-specific accidents. This is a recipe for confusion and mistakes.

Code drift. This was the most insidious problem. Over time, each repository accumulated small, undocumented differences. A different variable name here, an extra logging statement there, a local optimization that never got back-ported. The repositories were supposed to be 90% identical, but in reality they were 90% similar in slightly different ways.

The Catalyst: Serverless Framework Licensing

While the maintenance burden was growing, an external event added urgency to the consolidation effort. Serverless Framework announced licensing changes between version 3 and version 4, introducing commercial licensing requirements for organizations above a certain revenue threshold.

With fifteen repositories, each deploying multiple Lambda functions via Serverless Framework, our licensing exposure was fifteen times what a consolidated architecture would require. Consolidation wasn't just about maintenance efficiency anymore - it had direct financial implications.

This external pressure transformed the consolidation from "we should really do this someday" into "we need to start this now."

The Approach: Configuration Over Code Duplication

The consolidation strategy was built on a simple principle: differences between vendors should be expressed as configuration, not as code.

When we analyzed the fifteen codebases, the differences fell into predictable categories:

  • API endpoints - Each vendor has a different URL.
  • Authentication - Different credentials, sometimes different auth mechanisms.
  • Field mappings - The same data (name, email, ID) lives in different fields in different vendor APIs.
  • Minor logic variations - A few vendors needed specific data transformations or validation rules.
  • Scheduling - Different vendors preferred different processing schedules.

None of these differences justified separate codebases. They justified separate configuration files.

The Architecture: One Engine, Many Configs

The consolidated monorepo contains:

Shared service code - The "engine" for each service type (Welcome, Monthly, Register & Verify, Auto Renew, Recommends). This code handles all the common logic: API communication, error handling, retry mechanisms, logging, monitoring, data transformation pipelines.

Vendor configuration files - Each vendor has a configuration file (or set of files) that defines its specific parameters: endpoint URLs, field mappings, auth credentials, scheduling preferences, and any vendor-specific processing rules.

Configuration-driven runtime - At execution time, the service loads the appropriate vendor configuration based on environment variables or trigger parameters. The same Lambda function code runs for all vendors - only the configuration differs.

For the truly unique logic that a few vendors required, we built clean abstraction points: hook functions that the service calls at specific stages of processing. Most vendors use the default (no-op) hooks. The handful that need custom behavior provide their own implementations, isolated in their vendor configuration directory.

The Migration: Starting Small

We didn't attempt to migrate all fifteen repositories at once. That would have been reckless.

The migration followed a deliberate sequence:

Phase 1: Prove the pattern. We chose the Monthly service as the pilot because it was the most uniform across vendors. We migrated five vendors onto the shared Monthly codebase, validated that functionality was identical, and confirmed that the configuration-driven approach handled all vendor variations.

Phase 2: Expand the service. With the Monthly service proven, we established the template for migrating remaining services (Welcome, Register & Verify, etc.) and additional vendors.

Phase 3: Incremental vendor migration. Each vendor was migrated individually, with side-by-side testing against the original repository to verify output parity.

The key lesson here: migration is not a big bang. It's an incremental process where each step validates the architecture before you commit further.

Results: Fix Once, Deploy Everywhere

The impact was immediate and measurable:

Maintenance efficiency. A bug fix is now a single pull request, a single code review, and a single deployment that applies to all vendors. The days of fifteen-PR bug fixes are over.

Quality improvement. A shared test suite runs against all vendor configurations. A regression that affects one vendor's processing is caught before deployment, not discovered in production.

Faster vendor onboarding. Adding a new vendor no longer means cloning a repository and hoping you picked the right one to clone from. It means creating a configuration file that specifies the new vendor's endpoints, field mappings, and credentials. The service code is already written and tested.

Reduced infrastructure costs. Fewer Lambda functions, simpler CI/CD pipelines, and a single deployment process means less AWS spend and less operational overhead.

Reduced licensing exposure. The Serverless Framework footprint dropped dramatically with consolidation, simplifying the licensing picture for the v3-to-v4 transition.

Challenges We Encountered

The migration wasn't without friction. Here's what we ran into:

Truly unique vendor logic. While 90% of the code was shared, a few vendors had genuinely unique requirements - different data transformation pipelines, unusual API behaviors, vendor-specific error codes that needed special handling. We solved this with the abstraction point pattern: defined interfaces where vendor-specific code plugs in, keeping it isolated from the shared engine.

Configuration validation. When configuration drives behavior, bad configuration can break production. We invested in configuration validation - schema checks, test environments per vendor config, and CI gates that verify each vendor's configuration produces expected outputs against test data. A bad config file for vendor #7 must never affect vendors #1 through #6.

Historical debugging. When migrating code from fifteen repositories into one, the Git history gets disrupted. We preserved the original repositories (archived, read-only) as reference, so the team can trace the history of any vendor-specific decision back to its original context.

Team mindset shift. Developers accustomed to "my vendor's repository" had to shift to thinking about shared code that serves many vendors. This meant stricter coding standards, more thorough testing, and a heightened awareness that every change affects all vendors, not just one.

Lessons for Your Own Consolidation

If you're looking at your organization's repository landscape and seeing the same patterns - many repositories with mostly identical code - here's what we'd suggest:

Start with the most uniform service. Pick the service or component with the least variation across instances. Prove the consolidation pattern there, where the risk is lowest.

Define what "different" means explicitly. Before migrating, catalog every difference across repositories. Most of what feels like "code differences" is actually "configuration differences." The truly code-level differences are usually smaller than you think.

Invest in configuration validation early. In a monorepo, bad configuration is the new deployment risk. Build validation into your CI pipeline from day one, not as an afterthought.

Don't try to do everything at once. Migrate one service, one vendor at a time. Validate each step. The incremental approach is slower but dramatically safer.

Keep the old repositories accessible. Archive them as read-only references. The institutional knowledge encoded in Git history, PR discussions, and commit messages has value even after the code has moved.

From Fifteen to One

Repository consolidation is a force multiplier. Every improvement you make to the shared codebase - a performance optimization, a security patch, a new feature - automatically applies to all vendors. The compounding effect of maintaining one codebase instead of fifteen is difficult to overstate.

The key insight: what separates vendors is data (endpoints, mappings, credentials), not logic. Once you internalize that, consolidation becomes not just possible but obvious.

If you're managing multiple near-identical codebases and feeling the maintenance pain, [let's talk](https://www.droptica.com/contact/). We've been through the consolidation process and can help you plan and execute a migration that reduces complexity without disrupting operations.

M

Written by

Mautomic Team

The Mautomic team brings together experienced marketing automation specialists, developers, and consultants dedicated to helping businesses succeed with Mautic.

Need Help with Mautic?

Our team of experts is ready to help you implement and optimize your marketing automation.

Get in Touch