Monolithic applications eventually hit scaling limits. Microservices architecture breaks applications into small, independent services that communicate via APIs. At ZIRA Software, we've successfully migrated several large Laravel monoliths to microservices, achieving better scalability and team productivity.
Why Microservices?
Monolith problems:
- Single point of failure
- Difficult to scale specific features
- Deployment risk (everything at once)
- Technology lock-in
- Large codebase complexity
- Slow development velocity
Microservices benefits:
- Independent deployment
- Technology flexibility
- Better scalability
- Fault isolation
- Smaller, focused teams
- Easier testing
Trade-offs:
- Increased complexity
- Network latency
- Distributed debugging
- Data consistency challenges
- DevOps overhead
When to Use Microservices
Good candidates:
- Large, complex applications
- Multiple teams working independently
- Different scaling requirements per feature
- Need for technology diversity
- High availability requirements
Stick with monolith when:
- Small application
- Small team
- Rapid prototyping phase
- Simple deployment needs
Microservices Architecture
┌─────────────────────────────────────┐
│ API Gateway / Load │
│ Balancer (Nginx) │
└──────────┬──────────────────────────┘
│
┌──────┴──────┐
│ │
┌───▼───┐ ┌───▼────┐ ┌─────────┐
│ User │ │Product │ │ Order │
│Service│ │Service │ │ Service │
│ │ │ │ │ │
│MySQL │ │MySQL │ │ MySQL │
└───┬───┘ └───┬────┘ └────┬────┘
│ │ │
└────────────┴──────────────┘
Message Queue (RabbitMQ)
Building User Service
Directory Structure
user-service/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ └── UserController.php
│ │ └── Middleware/
│ ├── Models/
│ │ └── User.php
│ ├── Services/
│ │ └── UserService.php
│ └── Events/
│ └── UserCreated.php
├── routes/
│ └── api.php
├── database/
└── tests/
User Controller
<?php namespace App\Http\Controllers;
use App\Services\UserService;
use Illuminate\Http\Request;
class UserController extends Controller
{
protected $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
/**
* Get all users
*/
public function index()
{
$users = $this->userService->all();
return response()->json($users);
}
/**
* Get user by ID
*/
public function show($id)
{
$user = $this->userService->find($id);
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
return response()->json($user);
}
/**
* Create new user
*/
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
$user = $this->userService->create($request->all());
return response()->json($user, 201);
}
/**
* Update user
*/
public function update(Request $request, $id)
{
$user = $this->userService->update($id, $request->all());
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
return response()->json($user);
}
/**
* Delete user
*/
public function destroy($id)
{
$deleted = $this->userService->delete($id);
if (!$deleted) {
return response()->json(['error' => 'User not found'], 404);
}
return response()->json(null, 204);
}
}
User Service
<?php namespace App\Services;
use App\Models\User;
use App\Events\UserCreated;
use App\Events\UserUpdated;
use App\Events\UserDeleted;
class UserService
{
public function all()
{
return User::all();
}
public function find($id)
{
return User::find($id);
}
public function create(array $data)
{
$data['password'] = bcrypt($data['password']);
$user = User::create($data);
// Publish event to message queue
event(new UserCreated($user));
return $user;
}
public function update($id, array $data)
{
$user = User::find($id);
if (!$user) {
return null;
}
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$user->update($data);
event(new UserUpdated($user));
return $user;
}
public function delete($id)
{
$user = User::find($id);
if (!$user) {
return false;
}
$user->delete();
event(new UserDeleted($user));
return true;
}
}
Service Communication
1. RESTful HTTP
User Service calls Product Service:
<?php namespace App\Services;
use GuzzleHttp\Client;
class ProductServiceClient
{
protected $client;
protected $baseUrl;
public function __construct()
{
$this->baseUrl = env('PRODUCT_SERVICE_URL', 'http://product-service');
$this->client = new Client(['base_uri' => $this->baseUrl]);
}
public function getProduct($id)
{
try {
$response = $this->client->get("/api/products/{$id}");
return json_decode($response->getBody(), true);
} catch (\Exception $e) {
\Log::error('Product service error: ' . $e->getMessage());
return null;
}
}
public function getProductsByIds(array $ids)
{
try {
$response = $this->client->post('/api/products/batch', [
'json' => ['ids' => $ids]
]);
return json_decode($response->getBody(), true);
} catch (\Exception $e) {
\Log::error('Product service error: ' . $e->getMessage());
return [];
}
}
}
2. Message Queue (RabbitMQ)
Install PHP AMQP:
composer require php-amqplib/php-amqplib
Publish Event:
<?php namespace App\Events;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
class UserCreated
{
public $user;
public function __construct($user)
{
$this->user = $user;
$this->publish();
}
protected function publish()
{
$connection = new AMQPStreamConnection(
env('RABBITMQ_HOST', 'localhost'),
env('RABBITMQ_PORT', 5672),
env('RABBITMQ_USER', 'guest'),
env('RABBITMQ_PASSWORD', 'guest')
);
$channel = $connection->channel();
$channel->exchange_declare('user_events', 'fanout', false, true, false);
$message = new AMQPMessage(json_encode([
'event' => 'user.created',
'data' => $this->user->toArray(),
'timestamp' => now()->toISOString()
]));
$channel->basic_publish($message, 'user_events');
$channel->close();
$connection->close();
}
}
Consume Events (in Order Service):
<?php namespace App\Console\Commands;
use Illuminate\Console\Command;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class ConsumeUserEvents extends Command
{
protected $signature = 'queue:consume-user-events';
protected $description = 'Consume user events from RabbitMQ';
public function handle()
{
$connection = new AMQPStreamConnection(
env('RABBITMQ_HOST'),
env('RABBITMQ_PORT'),
env('RABBITMQ_USER'),
env('RABBITMQ_PASSWORD')
);
$channel = $connection->channel();
$channel->exchange_declare('user_events', 'fanout', false, true, false);
list($queue_name, ,) = $channel->queue_declare('', false, false, true, false);
$channel->queue_bind($queue_name, 'user_events');
$this->info('Waiting for user events...');
$callback = function ($msg) {
$data = json_decode($msg->body, true);
$this->info("Received: {$data['event']}");
// Process event
switch ($data['event']) {
case 'user.created':
$this->handleUserCreated($data['data']);
break;
case 'user.updated':
$this->handleUserUpdated($data['data']);
break;
}
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while ($channel->is_consuming()) {
$channel->wait();
}
$channel->close();
$connection->close();
}
protected function handleUserCreated($user)
{
// Cache user data locally
\Cache::put("user:{$user['id']}", $user, 3600);
$this->info("Cached user {$user['id']}");
}
protected function handleUserUpdated($user)
{
\Cache::put("user:{$user['id']}", $user, 3600);
$this->info("Updated cache for user {$user['id']}");
}
}
API Gateway
Nginx configuration:
upstream user-service {
server user-service:80;
}
upstream product-service {
server product-service:80;
}
upstream order-service {
server order-service:80;
}
server {
listen 80;
server_name api.example.com;
location /api/users {
proxy_pass http://user-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/products {
proxy_pass http://product-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/orders {
proxy_pass http://order-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Service Discovery
Using Consul:
<?php namespace App\Services;
use GuzzleHttp\Client;
class ServiceDiscovery
{
protected $consulUrl;
public function __construct()
{
$this->consulUrl = env('CONSUL_URL', 'http://consul:8500');
}
public function getServiceUrl($serviceName)
{
$client = new Client();
$response = $client->get("{$this->consulUrl}/v1/catalog/service/{$serviceName}");
$services = json_decode($response->getBody(), true);
if (empty($services)) {
throw new \Exception("Service {$serviceName} not found");
}
// Return first healthy service
$service = $services[0];
return "http://{$service['ServiceAddress']}:{$service['ServicePort']}";
}
}
// Usage
$discovery = new ServiceDiscovery();
$productServiceUrl = $discovery->getServiceUrl('product-service');
Health Checks
<?php
// routes/api.php
Route::get('/health', function() {
$healthy = true;
$checks = [];
// Database check
try {
DB::connection()->getPdo();
$checks['database'] = 'ok';
} catch (\Exception $e) {
$healthy = false;
$checks['database'] = 'failed';
}
// Cache check
try {
Cache::put('health_check', 'ok', 10);
$checks['cache'] = Cache::get('health_check') === 'ok' ? 'ok' : 'failed';
} catch (\Exception $e) {
$healthy = false;
$checks['cache'] = 'failed';
}
return response()->json([
'status' => $healthy ? 'healthy' : 'unhealthy',
'service' => 'user-service',
'checks' => $checks,
'timestamp' => now()->toISOString()
], $healthy ? 200 : 503);
});
Circuit Breaker Pattern
<?php namespace App\Services;
use Illuminate\Support\Facades\Cache;
class CircuitBreaker
{
protected $serviceName;
protected $failureThreshold = 5;
protected $timeout = 60; // seconds
public function __construct($serviceName)
{
$this->serviceName = $serviceName;
}
public function call(callable $callback)
{
$key = "circuit_breaker:{$this->serviceName}";
if (Cache::has("{$key}:open")) {
throw new \Exception("Circuit breaker is OPEN for {$this->serviceName}");
}
try {
$result = $callback();
// Reset failure count on success
Cache::forget("{$key}:failures");
return $result;
} catch (\Exception $e) {
$failures = Cache::increment("{$key}:failures");
if ($failures >= $this->failureThreshold) {
// Open circuit
Cache::put("{$key}:open", true, $this->timeout);
\Log::error("Circuit breaker OPENED for {$this->serviceName}");
}
throw $e;
}
}
}
// Usage
$breaker = new CircuitBreaker('product-service');
try {
$product = $breaker->call(function() use ($productId) {
return $this->productClient->getProduct($productId);
});
} catch (\Exception $e) {
// Fallback: return cached data or default
$product = Cache::get("product:{$productId}");
}
Database per Service
Each service has its own database:
# docker-compose.yml
services:
user-db:
image: mysql:5.7
environment:
MYSQL_DATABASE: users
MYSQL_ROOT_PASSWORD: secret
product-db:
image: mysql:5.7
environment:
MYSQL_DATABASE: products
MYSQL_ROOT_PASSWORD: secret
order-db:
image: mysql:5.7
environment:
MYSQL_DATABASE: orders
MYSQL_ROOT_PASSWORD: secret
Distributed Transactions (Saga Pattern)
<?php namespace App\Services;
class OrderSaga
{
protected $userService;
protected $productService;
protected $paymentService;
public function createOrder($userId, $products, $paymentDetails)
{
$compensation = [];
try {
// Step 1: Reserve inventory
$reservation = $this->productService->reserveInventory($products);
$compensation[] = function() use ($reservation) {
$this->productService->releaseReservation($reservation);
};
// Step 2: Charge payment
$payment = $this->paymentService->charge($paymentDetails);
$compensation[] = function() use ($payment) {
$this->paymentService->refund($payment);
};
// Step 3: Create order
$order = Order::create([
'user_id' => $userId,
'products' => $products,
'payment_id' => $payment->id,
'reservation_id' => $reservation->id
]);
return $order;
} catch (\Exception $e) {
// Compensate (rollback)
foreach (array_reverse($compensation) as $compensate) {
try {
$compensate();
} catch (\Exception $e) {
\Log::error('Compensation failed: ' . $e->getMessage());
}
}
throw $e;
}
}
}
Monitoring & Logging
Centralized logging with ELK:
<?php
// config/logging.php
'channels' => [
'logstash' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\SocketHandler::class,
'handler_with' => [
'connectionString' => 'tcp://logstash:5000',
],
'formatter' => Monolog\Formatter\LogstashFormatter::class,
'formatter_with' => [
'applicationName' => env('APP_NAME'),
],
],
],
// Usage
Log::channel('logstash')->info('Order created', [
'order_id' => $order->id,
'user_id' => $userId,
'service' => 'order-service'
]);
Best Practices
- Start with monolith - Migrate to microservices when needed
- Domain-driven design - Services based on business domains
- API versioning -
/api/v1/users,/api/v2/users - Async communication - Use message queues for non-critical operations
- Centralized logging - ELK stack or similar
- Distributed tracing - Track requests across services
- Service documentation - OpenAPI/Swagger specs
- Automated testing - Integration tests between services
- CI/CD pipeline - Independent deployments
- Monitoring - Prometheus, Grafana, or similar
Conclusion
Microservices solve real scaling problems but add complexity. At ZIRA Software, we've learned to start with a well-structured monolith and extract microservices only when clear benefits emerge. Done right, microservices enable independent scaling, technology choice, and team autonomy.
Considering microservices? Contact ZIRA Software to discuss architecture design, service decomposition strategies, and building scalable distributed systems.