Shared Kernel Pattern for Microservices with Symfony
AI generated
SF
{ }
Symfony · Shared Kernel · Microservices · Composer
Shared Kernel Pattern for Microservices
Shared code without locking services together

The shared kernel pattern solves a problem that appears in almost every Symfony microservice landscape: several services need the same value objects, error codes or event contracts. This article shows how a shared kernel is built as its own versioned Composer package, what belongs inside, what has to stay out, and how contract tests prevent creeping coupling.

19 min read Composer Package · Versioning · Contract Tests · Bounded Context Symfony 7.x · PHP 8.3+

1. Which problem the shared kernel pattern solves

As soon as a Symfony microservice landscape grows beyond two or three services, the same question tends to come up almost inevitably: where does the code live that several services need in common? A shared kernel pattern answers that with a deliberately small, jointly maintained slice of code that gets pulled into every participating service as its own versioned package. Without this explicit solution, shared code either ends up duplicated redundantly in every service, leading to inconsistencies, or it gets kept in sync through copy and paste, which is guaranteed to drift apart over time.

The term shared kernel comes from strategic Domain Driven Design and describes exactly this deliberately small, jointly agreed slice between two or more bounded contexts. The emphasis on small matters here. A shared kernel pattern that grows into a secret second domain model undermines exactly the independence microservices are supposed to provide. The following sections show how the boundary is drawn in practice and how a shared kernel is implemented technically as a Symfony compatible Composer package.

2. What belongs in the shared kernel, and what does not

The most important decision in the shared kernel pattern is scoping its content. Suitable candidates are pure value objects without behavior that must be interpreted identically across several services, for example a CustomerId class, a Money value object or standardized error code enums for API communication between services. These building blocks change rarely and carry no business logic tied to a single service.

Not suitable for a shared kernel pattern are aggregates, repository implementations or anything with a Doctrine dependency. As soon as a shared object enforces its own business rules or needs a database connection, it belongs in exactly one service, and other services call that service through an API instead of importing the code directly. This rule sounds simple but gets violated regularly in practice, when a team under time pressure quickly pushes an entity class into the shared kernel to avoid duplicate work, tying two services to the same implementation.


<?php

declare(strict_types=1);

namespace Mironsoft\SharedKernel\ValueObject;

// Shared Kernel content — pure value object, zero framework dependencies
final readonly class CustomerId
{
    private function __construct(public string $value) {}

    public static function fromString(string $value): self
    {
        if (!preg_match('/^cust_[a-z0-9]{16}$/', $value)) {
            throw new \InvalidArgumentException("Invalid CustomerId format: {$value}");
        }

        return new self($value);
    }

    public function equals(self $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

3. The shared kernel as its own Composer package

Technically, a shared kernel pattern is implemented most cleanly in a PHP microservice landscape as its own private Composer package, for example under the name mironsoft/shared-kernel, hosted in its own Git repository or through a private Packagist Satis repository. Every Symfony service that needs parts of it declares a dependency with a fixed version constraint in its composer.json, exactly like any other third party package.

This approach makes the shared kernel pattern explicitly visible. Anyone looking at a service's composer.json immediately sees which shared building blocks that service actually uses. That is a meaningful difference from a monorepo approach, where shared code is often pulled in implicitly through relative paths and the actual dependency disappears from view in the code, until a refactor unexpectedly touches several services at once.


{
  "name": "mironsoft/order-service",
  "require": {
    "php": "^8.3",
    "symfony/framework-bundle": "^7.1",
    "mironsoft/shared-kernel": "^2.4"
  },
  "repositories": [
    {
      "type": "composer",
      "url": "https://satis.mironsoft.internal"
    }
  ]
}

4. Versioning without breaking changes for every service

Because several services depend on the shared kernel pattern at the same time yet run on different deployment cycles, the package must strictly follow semantic versioning. A patch release changes nothing about the public interface, a minor release only adds additive functionality, and a major release signals breaking changes that every service can only follow after its own adaptation. Without this discipline, a single update of the shared kernel can potentially break half the microservice landscape at once.

In practice, a transition period proves useful for the shared kernel pattern. A new major release gets published, but the old major version stays maintained in parallel for a defined period and keeps receiving security patches. Services migrate one after another, not simultaneously, and a dashboard or a simple table shows visibly which version every service currently uses. This transparency prevents a forgotten service from running in production months later on a long outdated shared kernel version.

5. Event contracts as the stable part of the shared kernel

Besides value objects, the second common ingredient of a shared kernel pattern is the structure of integration events, meaning the messages services exchange over a message broker. Instead of every service maintaining its own interpretation of an OrderPlacedEvent, the shared kernel defines a single canonical PHP class with every field that consuming services can reliably expect, including an explicit event version in the payload.

This canonical structure in the shared kernel pattern prevents the most common integration problem in event driven microservice landscapes: different teams interpreting the same business event differently. It still matters that the event class itself contains no business logic, only data structure and format validation, so the shared kernel stays a pure contract instead of secretly turning into a second domain model that undermines the actual domain layer of the individual services.


<?php

declare(strict_types=1);

namespace Mironsoft\SharedKernel\Event;

// Shared Kernel content — canonical event contract, no business logic
final readonly class OrderPlacedEvent
{
    public function __construct(
        public string $eventVersion,
        public string $orderId,
        public string $customerId,
        public int $totalAmountInCents,
        public string $currency,
        public \DateTimeImmutable $occurredAt,
    ) {}

    public static function currentVersion(): string
    {
        return '2.0';
    }
}

6. Contract tests against silent drift

A shared kernel pattern without automated safeguards erodes just like any other architecture rule that relies purely on discipline. Contract tests solve this problem by letting the producing service guarantee in its own test suite that every event it actually sends matches the shared kernel schema, while consuming services verify in their own test suite that they can still handle the current contract correctly. Tools such as Pact are well suited for this kind of contract testing between services.

The practical benefit of contract tests in the shared kernel pattern shows up mainly in the CI pipeline. A team that accidentally removes a field from an event without bumping the major version gets that reported immediately as a failing contract test, long before the bug shows up in production for another team and has to be debugged there with much greater effort.

7. Governance: who is allowed to change the shared kernel?

Because a shared kernel pattern is by definition used jointly by several teams, it needs clear governance over who may propose and merge changes. A model that works well is a small group of codeowners drawn from different teams, who review every pull request against the shared kernel together, plus a fixed rule that breaking changes get announced in a short architecture meeting beforehand instead of simply being merged.

Without this governance, a shared kernel pattern tends to either stagnate because no one takes ownership, or grow uncontrollably because every team adds its own wishes without coordination. Both extremes undermine the actual purpose of the pattern, which is to have a deliberately small, stable, jointly agreed core that every participating service can rely on.

8. When a shared kernel is not worth it

The shared kernel pattern is not an automatic recommendation for every microservice landscape. For services run by completely different teams with different release rhythms, even a small shared dependency can turn into a coordination bottleneck, because every change to the shared kernel requires alignment across team boundaries. In such cases, duplicating a small value object across several services is often the more pragmatic choice than a shared dependency.

A second warning sign for the shared kernel pattern is size. If the package grows over time to several hundred classes, it is no longer effectively a shared kernel but a disguised shared library, bringing back exactly the coupling a microservice architecture is meant to avoid. A regular look at the size and scope of the package therefore belongs to maintaining the pattern, not just its technical versioning.

9. Shared kernel compared to alternatives

There are several strategies for sharing code between Symfony microservices. The following table compares the shared kernel pattern with the most common alternatives.

Strategy Coupling Consistency Best fit for
Copy paste duplication Very low Guaranteed to drift apart Very small, rarely changing value objects
Shared kernel package Low, versioned High, controlled Value objects and event contracts
Monorepo with direct imports High, implicit Very high Teams sharing the same deployment rhythm
Schema registry for events Low, language agnostic High Polyglot landscapes without shared PHP

For pure PHP Symfony landscapes, the shared kernel pattern as a Composer package usually offers the best trade off between consistency and low coupling. In polyglot systems with other languages besides PHP, a language agnostic schema registry for event contracts replaces or complements the Composer package.

Mironsoft

Symfony microservices, shared contracts and architecture governance

Shared code between services out of control?

We extract a clean shared kernel from existing scattered code, set up versioning and contract tests, and define the governance so your shared code stays small, stable and genuinely jointly owned.

Shared kernel extraction

Turn existing scattered code into a clean Composer package

Contract tests

Set up contract tests between services and integrate them into the CI pipeline

Governance model

Codeowner process and versioning rules for cross team code

10. Summary

The shared kernel pattern solves a real problem in every Symfony microservice landscape: shared value objects and event contracts need a stable, versioned home instead of being duplicated in every service or kept in sync implicitly. As its own Composer package with strict semantic versioning, contract tests and clear codeowner governance, the shared code stays small, stable and reliable for every participating team.

The most important success factor is discipline in scoping. Only pure data structures without business logic and without framework dependencies belong in the shared kernel. As soon as aggregates, repositories or Doctrine mapping slip in, the pattern turns into a hidden shared library that brings back exactly the coupling a microservice architecture is supposed to avoid. Teams that defend this boundary consistently get real value from the shared kernel pattern at minimal coupling cost.

Shared Kernel Pattern for Symfony Microservices — The Key Takeaways

Content

Pure value objects and event contracts, never aggregates, repositories or Doctrine mapping.

Distribution

A dedicated private Composer package with strict semantic versioning for every participating service.

Safeguards

Contract tests between producing and consuming services running in the CI pipeline.

Governance

Codeowners from several teams, breaking changes announced in advance, size reviewed regularly.

11. FAQ: Shared Kernel Pattern for Symfony Microservices

1What is a shared kernel in DDD?
A deliberately small, jointly agreed code slice between bounded contexts, usually value objects and event contracts.
2Are aggregates allowed inside?
No, aggregates stay bound to exactly one service, others call it through an API.
3How is it technically distributed?
As its own private Composer package through a Satis or Private Packagist repository.
4How do you prevent breaking changes for everyone?
Semantic versioning plus a transition period with parallel maintenance of the old major version.
5What are contract tests here?
Automated tests checking event schema conformance between producing and consuming services.
6Who should maintain it?
A small codeowner group from the participating teams with coordinated breaking changes.
7When is it not worth it?
With strongly diverging release rhythms between teams, where duplication is often more pragmatic.
8How large is it allowed to get?
As small as possible, otherwise it becomes a disguised library with high coupling.
9Does it fit polyglot systems?
A PHP package only works for PHP, otherwise a language agnostic schema registry is needed.
10Difference from a utility library?
Strategically scoped to domain contracts between bounded contexts, with explicit governance instead of arbitrary helpers.