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

  • gh CLI authenticated against Personio-Internal
  • curl and jq for 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-service for service consolidation

Or simply: "Help me pass the Microservices classified for consolidation Soundcheck check for holiday-calendar-service".

Example

  1. You ask Claude to classify your service.
  2. Claude validates the component exists in the Developer Portal and is in scope (type: microservice, lifecycle: production).
  3. Claude prints a prominent reminder to open the target-state spreadsheet, and waits for you to report what it says.
  4. Claude shows the current classification state and asks whether it matches the target.
  5. If a change is needed, Claude derives the source repo from backstage.io/source-location, sets up a worktree, edits .catalog.yml directly (no yq), and opens a draft PR labelled polaris-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.

PhaseStepSkillNotes
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
4Pre/sc-pre-requisite-phase4-checkVerify phases 1–3 are complete before starting Phase 4
41/sc-restructure-packagesMove classes to module-specific packages
42/sc-copy-secretsCopy secrets to the consolidated service in AWS
43/sc-env-prefixPrefix env vars to avoid consolidation conflicts
44/sc-update-app-propertiesUpdate Spring application properties to use prefixed env vars
45.1/sc-reorganise-propertiesReorganise property files into layered config architecture
45.2/sc-migrate-spring-cacheMigrate global spring.cache.* to namespaced spring.cache.<module>.*
45.3/sc-migrate-aws-propertiesMigrate global aws./sqs. properties to module-prefixed @ConfigurationProperties
46/sc-scope-databaseCreate isolated DataSource, TransactionManager, and Flyway beans
47.1/sc-update-infra-permissionsGrant consolidated service access to source service's infrastructure resources
47.2/sc-sync-k8s-configSync source service's HPA autoscaling and CPU/memory requests + startup delay into the target
48.1/sc-create-enable-annotationCreate @Enable<SourceService> annotation and restructure the source service Application class
48.2/sc-scope-schedulingScope @Scheduled tasks to a single deployable to avoid double execution
48.3/sc-import-moduleImport source service beans into the consolidated target service and resolve conflicts
49.1/sc-alias-cli-profileAdd a module-scoped alias for the source service's CLI/batch CronJob profile, without removing the original
49.2/sc-update-cli-triggersAdd the new alias profile to the CronJob's k8s-resources config and any Rundeck job, alongside the existing profile
49.3/sc-remove-cli-profileNarrow the source service's own code off the old CLI profile now that every trigger also requests the alias
51.1/sc-shift-grpcForward gRPC traffic per company to the consolidated service via a Split feature flag
51.2/sc-shift-httpForward REST (WebMVC) traffic per company to the consolidated service via a Split feature flag
51.3/sc-shift-webfluxForward REST (WebFlux) reactive traffic per company to the consolidated service via a Split feature flag
52.1/sc-shift-sqsForward 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 @SqsListenerBindingSqsForwardingMessageHandler, 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.