personio-eng-service-consolidation
Automates the service consolidation work for Personio engineers.
Installation
claude plugin install ./plugins/personio-eng-service-consolidation
What does this plugin do?
Helps engineers complete tasks tied to the Polaris service consolidation initiative:
- Classify a service in the Developer Portal to pass the Microservices classified for consolidation Soundcheck check.
- Restructure packages (Step 1 of consolidation): move all classes from generic
com.personio.*packages into module-specific packages to avoid naming conflicts when services are merged.
service-consolidation-classification
Walks through the 3-step classification process from the how-to guide: identify the service, check the target-state spreadsheet, inspect the current catalog entry, and open a PR against the service's .catalog.yml to add the right tag or annotations.
What you need
ghCLI authenticated against Personio-Internalcurlandjqfor catalog lookups- A clone (or write access to clone) of the target service's repository
- The target-state spreadsheet open in another tab — the skill will pause and remind you to check it
Quick Start
Ask Claude in natural language, naming the service:
Classify
holiday-calendar-servicefor service consolidation
Or simply: "Help me pass the Microservices classified for consolidation Soundcheck check for holiday-calendar-service".
Example
- You ask Claude to classify your service.
- Claude validates the component exists in the Developer Portal and is in scope (
type: microservice,lifecycle: production). - Claude prints a prominent reminder to open the target-state spreadsheet, and waits for you to report what it says.
- Claude shows the current classification state and asks whether it matches the target.
- If a change is needed, Claude derives the source repo from
backstage.io/source-location, sets up a worktree, edits.catalog.ymldirectly (noyq), and opens a draft PR labelledpolaris-service-consolidation.
Why would you want this?
- Keeps the classification flow consistent with the official how-to guide
- Asks before guessing — never silently picks a target service or invents a date
- Performs minimal, formatting-preserving edits to
.catalog.yml - Opens a draft PR with the correct label and reviewer, ready for a teammate review
How To Service Consolidation
The following document describes the steps to consolidate a service into our consolidated service.
Steps to Execute
After each step, create a Pull Request. It is important to execute the steps in order, as they build upon each other.
| Phase | Step | Skill | Notes |
|---|---|---|---|
| 1 | — | (no skill) | Co-locate source service into the monorepo: https://pzip.to/consolidation-phase1-step1 |
| 2 | — | (no skill) | Include source service in Gradle monorepo: https://pzip.to/consolidation-phase2-step1 |
| 3 | — | (no skill) | Initialize target consolidated service: https://pzip.to/consolidation-phase3-step1 |
| 4 | Pre | /sc-pre-requisite-phase4-check | Verify phases 1–3 are complete before starting Phase 4 |
| 4 | 1 | /sc-restructure-packages | Move classes to module-specific packages |
| 4 | 2 | /sc-copy-secrets | Copy secrets to the consolidated service in AWS |
| 4 | 3 | /sc-env-prefix | Prefix env vars to avoid consolidation conflicts |
| 4 | 4 | /sc-update-app-properties | Update Spring application properties to use prefixed env vars |
| 4 | 5.1 | /sc-reorganise-properties | Reorganise property files into layered config architecture |
| 4 | 5.2 | /sc-migrate-spring-cache | Migrate global spring.cache.* to namespaced spring.cache.<module>.* |
| 4 | 5.3 | /sc-migrate-aws-properties | Migrate global aws./sqs. properties to module-prefixed @ConfigurationProperties |
| 4 | 6 | /sc-scope-database | Create isolated DataSource, TransactionManager, and Flyway beans |
| 4 | 7.1 | /sc-update-infra-permissions | Grant consolidated service access to source service's infrastructure resources |
| 4 | 7.2 | /sc-sync-k8s-config | Sync source service's HPA autoscaling and CPU/memory requests + startup delay into the target |
| 4 | 8.1 | /sc-create-enable-annotation | Create @Enable<SourceService> annotation and restructure the source service Application class |
| 4 | 8.2 | /sc-scope-scheduling | Scope @Scheduled tasks to a single deployable to avoid double execution |
| 4 | 8.3 | /sc-import-module | Import source service beans into the consolidated target service and resolve conflicts |
| 4 | 9.1 | /sc-alias-cli-profile | Add a module-scoped alias for the source service's CLI/batch CronJob profile, without removing the original |
| 4 | 9.2 | /sc-update-cli-triggers | Add the new alias profile to the CronJob's k8s-resources config and any Rundeck job, alongside the existing profile |
| 4 | 9.3 | /sc-remove-cli-profile | Narrow the source service's own code off the old CLI profile now that every trigger also requests the alias |
| 5 | 1.1 | /sc-shift-grpc | Forward gRPC traffic per company to the consolidated service via a Split feature flag |
| 5 | 1.2 | /sc-shift-http | Forward REST (WebMVC) traffic per company to the consolidated service via a Split feature flag |
| 5 | 1.3 | /sc-shift-webflux | Forward REST (WebFlux) reactive traffic per company to the consolidated service via a Split feature flag |
| 5 | 2.1 | /sc-shift-sqs | Forward SQS messages per company to the consolidated service via two Split feature flags |
Step 1. Restructure Packages
In this step, we move all classes from generic packages to module-specific packages to avoid conflicts in consolidated mode.
/sc-restructure-packages <source-service> <base-package> <package-suffix>
Example: /sc-restructure-packages configuration-gateway-service com.personio.payroll configuration.gateway
Step 2. Copy Secrets
Analyze secrets used by the source service and produce a checklist of manual AWS Console
steps to copy them (with prefix) into the consolidated service's Secrets Manager. Must be
run for both dev and prod environments.
/sc-copy-secrets <source-service> <target-service> <PREFIX_>
Example: /sc-copy-secrets configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 3. Add Prefixed Environment Variables
Add $PREFIX_-prefixed environment variables to the source service configmap and populate
the target service's helm values, so both deployments can run without env var conflicts.
/sc-env-prefix <source-service> <target-service> <PREFIX_>
Example: /sc-env-prefix configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 4. Update Application Properties
Update Spring application*.yml files in the source service to reference the new
prefixed environment variables and secrets instead of the originals.
/sc-update-app-properties <source-service> <PREFIX_>
Example: /sc-update-app-properties configuration-gateway-service CONFIGURATION_GATEWAY_
Step 5.1. Reorganise Properties
Reorganise application property files into the layered architecture (base → module → deployment)
so the source service runs both standalone and inside the consolidated service without SpringPropertiesOverlapCheckConvention violations.
See the Layered Configuration Architecture page for the full architecture reference.
/sc-reorganise-properties <source-service> <target-service> <PREFIX_>
Example: /sc-reorganise-properties configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 5.2. Migrate Spring Cache Properties
Migrate global spring.cache.* properties to the namespaced spring.cache.<module>.* format so each module gets an isolated CacheManager when running inside the consolidated service.
Also updates @Cacheable/@CachePut/@CacheEvict annotations to reference the correct CacheManagerNames constant. Skips automatically if spring.cache.* is not present.
/sc-migrate-spring-cache <source-service> <target-service> <PREFIX_>
Example: /sc-migrate-spring-cache configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 5.3. Migrate AWS Properties
Migrate global aws.*/sqs.* properties to module-prefixed @ConfigurationProperties classes so SNS, SQS, and S3 configuration doesn't conflict between modules in the consolidated service.
Shared settings (aws.region, credentials, endpoint URLs) stay global in application.base.yml. Skips automatically if the source service has no aws.*, sqs.*, or personio.sqs.* properties.
/sc-migrate-aws-properties <source-service> <PREFIX_>
Example: /sc-migrate-aws-properties configuration-gateway-service CONFIGURATION_GATEWAY_
Step 6. Scope Database Connections
Create isolated, module-qualified DataSource, TransactionManager, JOOQ DSLContext, and Flyway beans via the DatabaseBeanRegistrar pattern so the source service's database wiring doesn't conflict in consolidated mode.
Also scopes the service's @Transactional annotations to the module's transaction manager, rewires audit logging if @EnableAuditing is used, and moves Flyway migrations to db/migration/<module>/.
Adds spring.autoconfigure.exclude to both the source service's application.<module>.yml and the target service's application.consolidated-overrides.yml so default Spring Boot datasource/JOOQ/Flyway/outbox wiring does not start up alongside the scoped beans.
Skips automatically if the source service has no spring.datasource.* properties.
/sc-scope-database <source-service> <target-service> <PREFIX_>
Example: /sc-scope-database configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 7.1. Update Infrastructure Permissions
Analyze the source service's infra-resources config and add permission-based entries to the consolidated service so it can access the source service's existing SQS queues, SNS topics, S3 buckets, and Kafka topics.
Uses permission grants (sqs_writer, sns_publisher, s3_writer, sqs_queue_reader, kafka_topic_reader) rather than resource-creating configs — these grant access to existing resources without creating new ones.
/sc-update-infra-permissions <source-service> <target-service>
Example: /sc-update-infra-permissions configuration-gateway-service payroll-native-germany-service
Step 7.2. Sync k8s Config
Reconcile the target (consolidated) service's Kubernetes/Helm config so it absorbs the source service's deployment
requirements — the HorizontalPodAutoscaler (min/max replicas, metric triggers, stabilization window) and the
kind: Microservice CPU/memory requests and startup probe delay — using a safe merge policy that never weakens the
target (each field takes the higher of source and target).
Like other k8s-resources changes, the edits land as a separate PR in the k8s-resources repo.
/sc-sync-k8s-config <source-service> <target-service>
Example: /sc-sync-k8s-config configuration-gateway-service payroll-native-germany-service
Step 8.1. Create Enable Annotation
Create the @Enable<SourceService> annotation and restructure the source service's Application class so its
beans are only activated when the annotation is explicitly applied. Copies all relevant annotations from
Application.kt onto the new annotation, carries over any @SpringBootApplication(exclude=[...]) exclusions,
and aligns @ConfigurationPropertiesScan base packages.
/sc-create-enable-annotation <source-service>
Example: /sc-create-enable-annotation configuration-gateway-service
Step 8.2. Scope Scheduled Tasks
Put each @Scheduled task behind a @ConditionalOnProperty so it has a single owner across the
standalone and consolidated deployables. Without this, the same scheduled job fires in both
deployables the moment the module is imported — double execution before any traffic is shifted.
Tasks are enabled in the source service and disabled in the consolidated service; in Phase 5 they
are enabled only in the consolidated service.
/sc-scope-scheduling <source-service> <target-service> <PREFIX_>
Example: /sc-scope-scheduling configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 8.3. Import Source Module
Import beans from the source service into the consolidated target service. Resolves bean name and type conflicts,
mocks JPA repositories and database infrastructure beans, scopes jOOQ generated classes to avoid package
collisions, qualifies SNS/SQS clients for consolidated mode, and wires the source module's DatasourceConfiguration
into the consolidated service.
/sc-import-module <source-service> <target-service> <PREFIX_>
Example: /sc-import-module configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 9.1. Alias CLI Profile
Some source services run a K8s CronJob that boots the app under a CLI/batch Spring profile (conventionally
named cli). Since multiple source services merging into the same target may all use a generic name like
cli, this step adds a module-scoped alias ($module-cli) alongside it, without removing the original —
so a later step can add the new name to the CronJob's Helm config and Rundeck job once this change is
deployed to prod.
/sc-alias-cli-profile <source-service> [profile-name]
Example: /sc-alias-cli-profile configuration-gateway-service
Step 9.2. Update CLI Triggers
Add the new profile alias created in Step 9.1 to everything that currently triggers the source service's CLI/batch profile.
- Kubernetes Cronjob resource(s) in
k8s-resources - Rundeck jobs in
rundeck
This is additive instead of replacement.
/sc-update-cli-triggers <source-service>
Example: /sc-update-cli-triggers configuration-gateway-service
Step 9.3. Remove CLI Profile
Remove the $old_profile_name (conventionally cli) as a CLI/batch profile trigger inside $source_service's module.
However, we keep the !$old_profile_name to ensure consolidated CLI/batch profile triggers do not spin up unnecessary Spring configuration.
Keeps the $new_profile_name as the new CLI/batch profile trigger. In tests, we update all @ActiveProfiles references to $old_profile_name to $new_profile_name.
/sc-remove-cli-profile <source-service>
Example: /sc-remove-cli-profile configuration-gateway-service
Phase 5 — Traffic Switch-over
Once phase 4 is complete and the source service runs as a module inside the consolidated service, phase 5 shifts live traffic from the source service to the consolidated service per company, controlled by Split feature flags. Both synchronous and asynchronous traffic are switched (see table above).
Step 1.1. Shift gRPC Traffic
Wire the personio-framework GrpcForwardingInterceptor into the source service so
that, for any company whose Split flag is ON, its incoming gRPC calls are
transparently forwarded to the consolidated service — no redeploy to flip a
company, no DNS/routing changes, instantly reversible. Adds the
commons-service-consolidation dependency, registers the interceptor as a global
gRPC server interceptor gated by a property, configures the channel to the
consolidated service and the per-company Split flag, then verifies end-to-end
(local mirrord + grpcurl smoke test, then per-company Datadog cutover).
Requires the personio-framework BOM 24.5.0+2026-06-23 or newer. Re-pointing
callers to the consolidated service is a separate, later consolidation step, out
of scope for this skill.
/sc-shift-grpc <source-service> <target-service> <PREFIX_>
Example: /sc-shift-grpc configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 1.2. Shift REST (WebMVC) Traffic
The servlet (Spring WebMVC) counterpart of step 1.1. Wire the personio-framework
WebMVCForwardingFilter into the source service so that, for any company whose
Split flag is ON, its incoming REST requests are transparently forwarded to the
consolidated service — no redeploy to flip a company, no DNS/routing changes,
instantly reversible. Adds the commons-service-consolidation dependency,
registers the filter as a FilterRegistrationBean ordered after Spring Security
and gated by a property, configures the consolidated targetBaseUrl and the
per-company Split flag, then verifies end-to-end (local mirrord + curl smoke
test, then per-company Datadog cutover). The HTTP client is pluggable — the skill
auto-detects whether the service uses RestTemplate or RestClient.
Requires the personio-framework BOM 24.5.0+2026-06-23 or newer. Re-pointing
callers to the consolidated service is a separate, later consolidation step, out of
scope for this skill.
/sc-shift-http <source-service> <target-service> <PREFIX_>
Example: /sc-shift-http configuration-gateway-service payroll-native-germany-service CONFIGURATION_GATEWAY_
Step 1.3. Shift REST (WebFlux) Traffic
The reactive (Spring WebFlux) counterpart of step 1.2. Wire the personio-framework
WebFluxForwardingFilter into the source service so that, for any company whose
Split flag is ON, its incoming reactive REST requests are transparently forwarded to
the consolidated service — no redeploy to flip a company, no DNS/routing changes,
instantly reversible. Adds the commons-service-consolidation dependency, registers
the filter as a reactive WebFilter @Bean ordered after Spring Security and gated
by a property, configures the consolidated targetBaseUrl and the per-company Split
flag, then verifies end-to-end (local mirrord + curl smoke test, then per-company
Datadog cutover). The forwarding client is the WebClient-based
WebClientForwardingHttpClient — pass a dedicated, plain WebClient.create(), never
the framework's credential-attaching one.
Because WebFlux resolves the company via explicit multitenancy, a service that
also exposes gRPC must add explicit-multitenancy-spring-grpc to its classpath (and
personio.multitenancy.explicit.disable=true does not work on pure WebFlux).
Requires the personio-framework BOM 24.5.0+2026-06-23 or newer. Re-pointing
callers to the consolidated service is a separate, later consolidation step, out of
scope for this skill.
/sc-shift-webflux <source-service> <target-service> <PREFIX_>
Example: /sc-shift-webflux notifications-service payroll-native-germany-service NOTIFICATIONS_
Step 2.1. Shift SQS Traffic
The asynchronous (SQS) counterpart of the step-1 synchronous skills. Because SNS
fan-out delivers a copy of every message to both deployments, SQS uses two
per-company Split flags (source + consolidated) and the personio-framework
SqsForwardingMessageHandler decorator, which evaluates both flags so each side
decides whether to process or discard its own copy — with no silent-loss window
during the ramp. Adds the commons-service-consolidation dependency, auto-detects
the consumer wiring model (manual @SqsListenerBinding → SqsForwardingMessageHandler,
or starter @Listens multi-resource → SqsForwardingBeanPostProcessor), binds
SqsForwardingProperties via Binder, mirrors the source queue in infra-resources
behind a placeholder filter, declares the sqs.consolidation.both_off Datadog
monitor as infra-resources IaC, then verifies end-to-end (dev four-cell
decision-matrix walk in Datadog) and covers cutover.
Requires the personio-framework BOM 24.5.0+2026-06-23 or newer. Handlers must
be idempotent (the (ON,ON) bridge double-processes). Queue cutover and
re-pointing producers are separate later steps, out of scope for this skill.
/sc-shift-sqs <source-service> <target-service> <PREFIX_>
Example: /sc-shift-sqs calculator-service payroll-native-germany-service CALCULATOR_
Warmup self-calls — current hypothesis (TODO: validate with KDX)
Some services run a warmup on boot where the service calls itself (a client hitting its own endpoints) to instantiate lazy objects. How does per-company traffic switching interact with that?
Current hypothesis: warmup self-calls almost always run without a company
principal, so PrincipalDataProvider resolves "anonymous" → the per-company
Split flag is OFF for "anonymous" → the call is not forwarded and warms the
local instance as intended. It would only forward if the warmup ran under a real
company that already has the flag ON — in which case add that warmup path to the
filter's excludedPaths (HTTP/WebFlux) or exclude it on the interceptor (gRPC). The
same reasoning applies to sc-shift-grpc and sc-shift-webflux.
TODO (KDX): this is unverified. We need to test it against the actual warmup implementations and discuss within KDX before baking guidance into the skills. Until then, treat the above as a hypothesis, not a guarantee.