Adding more developers to a late project makes it later. Fred Brooks wrote that in The Mythical Man-Month in 1975, and it has been proven true countless times since. The reason is not that developers are unproductive. The reason is communication overhead.
Brooks defined group communication with the formula n(n-1)/2, where n is the number of people on a team. Four people have six communication channels. Ten people have forty-five. Fifty-three people have 1,378. Every additional team member adds communication channels that consume time and cognitive energy.
Microservices solve this problem at the architectural level. Instead of one giant team working on one giant application, microservices enable small, focused teams to own small, focused services. Each team communicates within a small group. Cross-team communication happens through well-defined API contracts.
This article explores the why and how of building microservices from a PHP perspective, with practical examples and lessons from real-world implementations.
What You’ll Learn
- How Fred Brooks’ communication formula drives microservices adoption
- Why small teams produce better software than large teams
- How to structure PHP microservices as independent applications
- Service discovery patterns with practical PHP implementations
- Language and tooling flexibility with microservices
- Container deployment strategies for PHP microservices
- Common pitfalls and how to avoid them
The Communication Problem
The formula n(n-1)/2 describes the number of potential communication channels in a group of n people. On a team of four developers, there are six channels. Information flows quickly. Decisions are made efficiently. Everyone knows what everyone else is doing.
On a team of ten, there are forty-five channels. Meetings take longer. More people need to be consulted. Information gets lost. Developers make changes without knowing that another developer is working on the same area. Coordination overhead eats into productive time.
On a team of fifty-three — which is what you get when five engineering teams of roughly ten people each are effectively one giant team — there are 1,378 channels. This is not sustainable. Communication overhead dominates the work.
How Microservices Help
Microservices break the large team into smaller teams, each owning a specific business domain. A five-person team has only ten communication channels. Cross-team communication funnels through designated representatives rather than requiring everyone to talk to everyone.
<?php
// In a monolithic application, every developer might touch this code
class OrderProcessor
{
public function __construct(
private PaymentGateway $payment,
private InventoryService $inventory,
private ShippingService $shipping,
private NotificationService $notifications,
private AuditService $audit,
) {}
public function process(Order $order): void
{
$this->payment->charge($order);
$this->inventory->reserve($order->items);
$this->shipping->schedule($order);
$this->notifications->sendConfirmation($order);
$this->audit->logOrder($order);
}
}With microservices, each of these responsibilities becomes its own service owned by its own team. The order processor service calls other services via HTTP or message queues. No single team needs to understand every subsystem in detail.
Best Tool for the Job
One of the strongest arguments for microservices is language and tooling flexibility. Monolithic applications typically use a single language and runtime. If that runtime becomes outdated, upgrading requires touching every part of the application.
Microservices allow each team to choose the best tool for their domain. A PHP team might own the order processing service while a Python team owns the recommendation engine and a Go team owns the real-time notification system.
<?php
// A PHP microservice for order processing
// Can be deployed independently from other services
use Symfony\Component\HttpClient\HttpClient;
class OrderService
{
public function createOrder(array $items, string $userId): Order
{
// Validate inventory via the Inventory Service (could be any language)
$inventoryClient = HttpClient::createForBaseUri(
$this->serviceDiscovery->getUrl('inventory-service')
);
$response = $inventoryClient->request('POST', '/api/reserve', [
'json' => ['items' => $items],
]);
if ($response->getStatusCode() !== 200) {
throw new InventoryException('Could not reserve items');
}
// Create the order in our own database
$order = Order::create([
'user_id' => $userId,
'items' => $items,
'status' => 'pending',
]);
return $order;
}
}This flexibility extends to framework choices within PHP. One service can use Laravel while another uses Symfony. Each team upgrades their dependencies on their own schedule. The PHP 7.3 service can coexist with the PHP 8.2 service because they run in separate containers.
Microservices in Practice
Let’s build a practical example. A blog application is split into microservices. Each service is a standalone PHP application with its own database, its own deployment pipeline, and its own team.
Service Structure
/path/to/microservices/
├── users/
├── posts/
└── service-discovery/
Posts Microservice
The posts service manages blog posts. It is a standard PHP application using any framework. We use pocket-framework for simplicity, but Symfony, Laravel, or Slim would work identically.
<?php
// src/Controller/ApiController.php
#[RouteGroup('/api/posts')]
class PostsApiController
{
public function __construct(
protected PostService $postService,
) {}
#[RouteInfo('/')]
public function list(): JsonResponse
{
return new JsonResponse([
'_embedded' => [
'posts' => $this->postService->all(),
],
]);
}
#[RouteInfo('/{id}')]
public function get(string $id): JsonResponse
{
try {
return new JsonResponse(
$this->postService->find($id)
);
} catch (RuntimeException $e) {
return new JsonResponse(
['title' => 'Not Found', 'detail' => 'Post not found'],
404
);
}
}
#[RouteInfo('/', methods: ['POST'])]
public function create(ServerRequestInterface $request): JsonResponse
{
$data = json_decode(
$request->getBody()->getContents(),
true
);
$post = $this->postService->create(
$data['title'],
$data['content'],
$data['author_id']
);
return new JsonResponse($post, 201);
}
}Run it locally:
php -S localhost:8080 -t public/Users Microservice
The users service handles authentication, user management, and token validation. It is its own application with its own database schema.
<?php
// src/Controller/ApiController.php
#[RouteGroup('/api/users')]
class UsersApiController
{
public function __construct(
protected UserService $userService,
) {}
#[RouteInfo('/')]
public function list(): JsonResponse
{
return new JsonResponse([
'_embedded' => [
'users' => $this->userService->all(),
],
]);
}
#[RouteInfo('/', methods: ['POST'])]
public function create(ServerRequestInterface $request): JsonResponse
{
$data = json_decode(
$request->getBody()->getContents(),
true
);
$user = $this->userService->create(
$data['email'],
$data['password'],
$data['name']
);
return new JsonResponse($user, 201);
}
#[RouteInfo('/{id}/validate', methods: ['POST'])]
public function validate(ServerRequestInterface $request, string $id): JsonResponse
{
$data = json_decode(
$request->getBody()->getContents(),
true
);
$user = $this->userService->find($id);
if (password_verify($data['password'], $user['password'])) {
return new JsonResponse([
'token' => $this->userService->generateToken($id),
]);
}
return new JsonResponse(['error' => 'Invalid credentials'], 401);
}
}Run on port 8081:
php -S localhost:8081 -t public/Service Discovery
Now we have two services. They do not know about each other. If the posts service needs to validate a user token, how does it find the users service? This is the service discovery problem.
Service discovery can take many forms:
- DNS-based: Services register DNS entries that resolve to their addresses
- Key-value store: Services register themselves in etcd, Consul, or ZooKeeper
- Platform-native: Kubernetes and Docker Compose provide built-in service discovery
- Custom API: A lightweight registration service
For our example, we build a simple service discovery API.
<?php
// Service Discovery Service
#[RouteGroup('/api/services')]
class ServiceDiscoveryController
{
protected array $services = [];
public function __construct()
{
$file = __DIR__ . '/../data/services.json';
if (file_exists($file)) {
$this->services = json_decode(
file_get_contents($file),
true
);
}
}
#[RouteInfo('/')]
public function list(): JsonResponse
{
return new JsonResponse([
'_embedded' => [
'services' => $this->services,
],
]);
}
#[RouteInfo('/', methods: ['POST'])]
public function register(ServerRequestInterface $request): JsonResponse
{
$data = json_decode(
$request->getBody()->getContents(),
true
);
$this->services[$data['name']][] = [
'address' => $data['address'],
];
file_put_contents(
__DIR__ . '/../data/services.json',
json_encode($this->services)
);
return new JsonResponse($data, 201);
}
#[RouteInfo('/{name}')]
public function get(string $name): JsonResponse
{
return new JsonResponse(
$this->services[$name] ?? []
);
}
}Register the services:
curl -X POST http://localhost:8082/api/services/ \
-H "Content-Type: application/json" \
-d '{"name": "posts", "address": "http://localhost:8080"}'
curl -X POST http://localhost:8082/api/services/ \
-H "Content-Type: application/json" \
-d '{"name": "users", "address": "http://localhost:8081"}'Now the posts service can discover and call the users service at runtime.
<?php
// Posts Service with service discovery
#[RouteGroup('/api/posts')]
class PostsApiController
{
protected array $userService = [];
public function __construct(
protected PostService $postService,
) {
$discoveryUrl = getenv('DISCOVERY_URL')
?: 'http://localhost:8082';
$client = new \GuzzleHttp\Client();
$response = $client->get(
"$discoveryUrl/api/services/users"
);
$services = json_decode(
$response->getBody()->getContents(),
true
);
$this->userService = $services[0] ?? [];
}
#[RouteInfo('/', methods: ['POST'])]
public function create(ServerRequestInterface $request): JsonResponse
{
$data = json_decode(
$request->getBody()->getContents(),
true
);
// Validate the user through the users service
$auth = $request->getHeader('Authorization');
$token = str_replace('Bearer ', '', $auth[0] ?? '');
$client = new \GuzzleHttp\Client();
$response = $client->post(
$this->userService['address'] . '/api/users/' . $data['author_id'] . '/validate',
['json' => ['token' => $token]]
);
if ($response->getStatusCode() !== 200) {
return new JsonResponse(['error' => 'Unauthorized'], 403);
}
$post = $this->postService->create(
$data['title'],
$data['content'],
$data['author_id']
);
return new JsonResponse($post, 201);
}
}Deploying Microservices with Containers
Docker is the standard deployment mechanism for microservices. Each service has its own Dockerfile, its own image, and its own deployment configuration.
# Dockerfile for posts service
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y \
libpq-dev \
unzip \
git \
&& docker-php-ext-install pdo_pgsql
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY . .
RUN composer install --no-dev --optimize-autoloader
EXPOSE 8080
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public/"]Docker Compose ties everything together for local development:
# docker-compose.yml
version: '3.8'
services:
posts:
build:
context: ./posts
ports:
- "8080:8080"
depends_on:
- posts-db
- service-discovery
users:
build:
context: ./users
ports:
- "8081:8081"
depends_on:
- users-db
- service-discovery
posts-db:
image: postgres:15
environment:
POSTGRES_DB: posts
users-db:
image: postgres:15
environment:
POSTGRES_DB: users
service-discovery:
build:
context: ./service-discovery
ports:
- "8082:8082"Real-World Use Cases
Scaling a SaaS Platform
A SaaS platform initially built as a monolith faces scaling challenges. The reporting module is CPU-intensive. The notification module is I/O-bound. Breaking these into separate services allows independent scaling. The reporting service runs on compute-optimized instances while the notification service uses memory-optimized instances.
Multi-Team Product Development
A product with distinct domains — billing, user management, content, analytics — assigns each domain to a separate team. Each team owns their service end-to-end, from database schema to deployment. Cross-team integration happens through well-defined APIs.
Legacy Modernization
A legacy PHP application is migrated incrementally. New features are built as microservices. Existing monolith features are extracted one at a time. Each extraction improves testability, deployability, and team autonomy.
Best Practices
Start Small
Do not start with twenty microservices. Start with two or three. Let the architecture emerge from the team and organizational structure, not from a whiteboard diagram.
Define Clear API Contracts
Every microservice needs a well-defined API contract. Use OpenAPI specifications. Version your APIs from day one. Breaking changes should never surprise downstream consumers.
Own Your Data
Each microservice owns its database. No two services share a database. If another service needs data, it must go through the owning service’s API. This prevents tight coupling at the data layer.
Invest in Observability
Distributed systems fail in distributed ways. Invest in centralized logging, distributed tracing, and metrics collection from the start. You cannot debug what you cannot see.
Automate Everything
Manual deployment does not scale. Automate builds, tests, deployments, and rollbacks. Use CI/CD pipelines that give each team deployment independence.
Common Mistakes to Avoid
Starting with microservices. A monolith is better for early-stage products. Microservices add operational complexity that you do not need until team size justifies it.
Shared databases. Services sharing a database are not truly decoupled. Schema changes require coordination across teams.
Too fine-grained services. Each microservice should represent a meaningful business domain. Do not split services so small that a single feature spans ten services.
Ignoring network latency. Local method calls become network calls. Design APIs to minimize round trips. Consider batch endpoints and asynchronous patterns.
Inconsistent error handling. Each service should return errors in a consistent format. Define a standard error response structure across all services.
Frequently Asked Questions
When should a PHP team adopt microservices?
When your team exceeds ten developers, when different parts of the application need different deployment schedules, or when you need to scale components independently. Do not adopt microservices for a three-person team building a simple CRUD app.
Can PHP handle microservices performance requirements?
Yes. PHP microservices with OPcache and modern frameworks handle thousands of requests per second. Services that need higher throughput can use RoadRunner or Swoole for persistent PHP runtimes.
How do microservices handle database transactions?
They do not. Distributed transactions across services are an anti-pattern. Each service manages its own data consistency. Use sagas or event-driven patterns for multi-service workflows.
What about inter-service authentication?
Use API tokens, JWT, or OAuth2. Each request between services should carry authentication context. Service meshes like Istio can handle mutual TLS and authentication at the infrastructure level.
How do I debug issues across multiple services?
Use correlation IDs that propagate through every service call. Centralized logging with Elasticsearch or Loki. Distributed tracing with OpenTelemetry or Jaeger. These tools make cross-service debugging manageable.
Conclusion
Microservices solve a communication problem, not a technology problem. Fred Brooks’ n(n-1)/2 formula shows why large teams on monolithic applications struggle with coordination overhead. By breaking applications into domain-aligned services owned by small teams, microservices reduce communication channels and increase development velocity.
PHP is a capable language for building microservices. Each service is a standard PHP application with its own framework, its own database, and its own deployment pipeline. Service discovery, container deployment, and API contracts tie everything together.
The key is to start small. Do not architect a complete microservices system on day one. Let your team structure and application complexity guide your architecture decisions. When the monolith becomes painful, extract your first service.
Ready to start your microservices journey? Take one bounded context from your monolithic application — user authentication, notification delivery, or report generation — and extract it as a standalone PHP service. Deploy it in a container. Let your team experience the independence microservices provide.