Continuous Integration in a Low-Code Platform: Making CI Work Where It Wasn't Designed To
In 2017, I was working on a large-scale implementation built on a low-code iPaaS (integration Platform as a Service) that handles workflow automation, case management, and business process orchestration. The platform is powerful for what it’s designed to do. What it was not designed for was continuous integration.
This is the story of how we made it work anyway.
The Problem With Low-Code and CI
Traditional CI pipelines assume you have source code. You write code, commit it, a build server picks it up, compiles it, runs tests, and produces an artifact. Simple enough.
The platform doesn’t work that way. In this platform, most of your “application” lives inside the platform itself. process models, rules, expression rules, interfaces, record types, constants, and data stores are all configured through a visual designer. There’s no .java file to compile. There’s no unit test framework built in. Your application is the platform state.
This creates a fundamental tension: you want modern engineering practices (automated testing, repeatable builds, environment promotion), but the platform treats your application as a living, stateful thing that exists inside a running server.
We had a growing team, multiple workstreams, and releases that were getting increasingly painful. Manual testing was eating weeks. Environment drift was causing “works on my machine” bugs. except the “machine” was a shared development environment. Something had to change.
What We Actually Built
Test Strategy
Without built-in unit testing, we had to get creative. Our approach was layered:
Expression rule testing. The platform’s expression rules are pure functions. Given inputs, they produce outputs. We wrote a test harness that would invoke expression rules via the platform’s API, pass in test fixtures, and assert on results. This covered our business logic layer reasonably well.
Process model smoke tests. For workflow logic, we built lightweight process models whose sole purpose was to exercise critical paths of production process models. These would start a case, advance it through key decision points, and verify the end state. Not full integration tests, but enough to catch regressions.
Database validation. More on this below with Liquibase, but we had automated checks that schema state matched expected state after migrations.
API-level integration tests. The platform exposes web APIs for its objects. We wrote a suite of tests that would exercise these endpoints after deployment, verifying that the plumbing between the platform and external systems was intact.
The Build: Packaging Platform Applications
The platform provides an export mechanism. you can export application packages as .zip files containing XML definitions of your objects. This became our “build artifact.” We wrote scripts that would:
- Trigger an export via the platform’s admin API
- Pull the resulting package
- Version it in Git alongside our custom code (Java plugins, SQL migrations, configs)
- Tag the whole thing as a release candidate
It wasn’t elegant, but it gave us something we didn’t have before: a versioned, reproducible snapshot of the application state.
Liquibase for Database Migrations
This was actually the most straightforward piece. The platform sits on top of a relational database (we used MySQL), and we had significant custom schema beyond what the platform manages internally. Liquibase was the obvious choice.
Our changelog structure looked roughly like this:
<databaseChangeLog>
<include file="changelogs/001-initial-schema.xml"/>
<include file="changelogs/002-add-case-tracking.xml"/>
<include file="changelogs/003-reporting-views.xml"/>
<!-- Each sprint added new changesets -->
</databaseChangeLog>
Every migration was idempotent and tested in isolation before being applied. We ran Liquibase as part of the CI pipeline. after deploying the platform package but before running integration tests. This ordering mattered: the platform data stores expect the schema to exist, and our tests expected both the platform and the schema to be in sync.
The tricky part was coordinating the platform’s own schema expectations with our custom tables. The platform manages its internal tables automatically, and occasionally platform upgrades would shift things around. We learned to keep our custom schema completely isolated from the platform’s internal tables and use views for any cross-cutting queries.
The CI Pipeline
Here’s what the pipeline looked like:
graph TD
A[Developer commits code/config] --> B[Jenkins detects change]
B --> C[Build Java plugins]
C --> D[Export platform application package]
D --> E[Deploy to CI environment]
E --> F[Run Liquibase migrations]
F --> G[Deploy platform package to CI server]
G --> H[Wait for platform to sync objects]
H --> I[Run expression rule tests]
I --> J[Run process model smoke tests]
J --> K[Run API integration tests]
K --> L{All tests pass?}
L -->|Yes| M[Tag release candidate]
L -->|No| N[Notify team, block promotion]
Notice step H. “Wait for the platform to sync objects.” This was a real thing. After importing a package, the platform needs time to process and activate the objects. There’s no webhook or completion event. We polled an API endpoint until key objects were detectable, with a timeout that would fail the build if sync took too long.
The Hard Parts
Environment Management
This was our biggest headache. The platform environments are heavyweight. Each one is a full application server deployment with its own database, its own configuration, and its own state. You can’t spin one up in 30 seconds like a Docker container.
We maintained dedicated CI environments, but they were precious resources. If a build failed and left the environment in a dirty state, someone had to manually clean it up before the next build could run. This meant builds were serialized. no parallel CI runs.
Object Conflicts
The platform’s objects have UUIDs, but they also have name-based references. When two developers modified the same process model on different branches, merging was… not a merge. It was “pick one and re-apply the other’s changes manually.” The XML inside the export packages wasn’t designed for diff/merge workflows.
We eventually adopted a strict ownership model: each process model had one owner, and if you needed to change someone else’s model, you coordinated verbally first. Not sophisticated, but it worked.
Test Flakiness
Expression rule tests were rock solid. Everything else was flaky. Process model tests would fail because of timing issues. a step that usually completed in 2 seconds would occasionally take 30. API tests would fail because the platform’s internal caching hadn’t refreshed yet.
We built retry logic with exponential backoff into everything. It helped, but “re-run the build” was still a weekly occurrence.
No First-Class Plugin Testing
The platform supports Java plugins for custom functions and smart services. These could be unit tested traditionally with JUnit, and we did. But testing them within the platform. verifying that the plugin integrated correctly with the platform. required a running platform instance. There was no local development mode, no embedded server, no test harness.
What We Learned
Low-code doesn’t mean low-complexity. The visual designers abstract away syntax, but the systems you build on these platforms are every bit as complex as traditionally coded ones. Maybe more, because the tooling assumes you won’t need CI/CD.
Invest in environment automation early. Our biggest time sink was environment management. If I could do it over, I would have prioritized environment provisioning automation from day one. Which, as it turns out, is exactly what we did next.
Liquibase is your friend. Even in a low-code world, database migrations need rigor. Liquibase gave us the confidence to evolve our schema without fear.
Build the test harness you wish existed. No built-in testing? Build it. The time investment paid for itself within the first month of catching regressions before they hit QA.
What Came Next
Getting CI working was a win, but we were still constrained by heavyweight environments and serialized builds. The next step was obvious in retrospect, though it felt radical at the time: we containerized the entire platform with Docker.
In 2017, Docker was still relatively new in enterprise settings. Containerizing a distributed system like this. with its application server, search engine, and database components. was uncharted territory. But it was the only way to get the fast, disposable environments we needed for real CI/CD.
That’s Part 2: Containerizing a Low-Code iPaaS for CI/CD.
Related
- Containerizing a Low-Code iPaaS for CI/CD: the next chapter: stuffing a distributed enterprise platform into Docker
- From 4-Hour Deploy Hell to 20-Minute Victory Laps: applying the same incremental-CI playbook to a different team’s deploy nightmare