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.
Table of Contents
- 1. Which problem the shared kernel pattern solves
- 2. What belongs in the shared kernel, and what does not
- 3. The shared kernel as its own Composer package
- 4. Versioning without breaking changes for every service
- 5. Event contracts as the stable part of the shared kernel
- 6. Contract tests against silent drift
- 7. Governance: who is allowed to change the shared kernel?
- 8. When a shared kernel is not worth it
- 9. Shared kernel compared to alternatives
- 10. Summary
- 11. FAQ
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.