Microservices Migration: PHP to Java Spring Boot at Rakuten
Migrating a live e-commerce platform generating $200M+ annually from a monolithic PHP architecture to Java Spring Boot microservices is no small feat. Here's our journey at Rakuten Beauty.
The Legacy System
Rakuten Beauty's platform was built on:
- Monolithic PHP codebase (~500K LOC)
- MySQL database (single instance)
- Manual deployment process
- 2-3 hour deployment windows
- Tight coupling between features
Pain Points
- Scalability: Couldn't scale specific features independently
- Deployment: High-risk, infrequent deployments
- Performance: Slow response times during peak hours
- Development Speed: Features took months to ship
- Technology Stack: Difficulty hiring PHP developers
Migration Strategy
We adopted the Strangler Fig Pattern - gradually replacing parts of the legacy system while maintaining business continuity.
Phase 1: Assessment (2 months)
┌─────────────────────────────────┐
│ Legacy PHP Monolith │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │User │ │Order │ │Pay │ │
│ │Mgmt │ │Mgmt │ │ment │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ ┌──────────┐ │
│ │ MySQL │ │
│ └──────────┘ │
└─────────────────────────────────┘
Key Activities:
- Domain analysis and bounded context identification
- API contract definition
- Database dependency mapping
- Risk assessment
Phase 2: Infrastructure Setup (1 month)
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
template:
spec:
containers:
- name: user-service
image: rakuten/user-service:latest
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
Infrastructure Components:
- Kubernetes cluster on AWS EKS
- API Gateway (Kong)
- Service mesh (Istio)
- Monitoring (Prometheus + Grafana)
- Logging (ELK Stack)
Phase 3: Parallel Development (6 months)
We started with the Authentication Service - a clear bounded context with minimal dependencies.
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(
@RequestBody LoginRequest request
) {
AuthResponse response = authService.authenticate(
request.getEmail(),
request.getPassword()
);
return ResponseEntity.ok(response);
}
}
Database Strategy: Dual Writes
@Transactional
public User createUser(UserDTO dto) {
// Write to new DB
User user = userRepository.save(dto.toEntity());
// Write to legacy DB
legacyUserRepository.save(user);
return user;
}
Phase 4: Traffic Switching (3 months)
Gradual traffic migration using feature flags:
@Service
public class UserServiceRouter {
@Value("${feature.new-user-service.enabled}")
private boolean newServiceEnabled;
@Value("${feature.new-user-service.percentage}")
private int rolloutPercentage;
public User getUser(String userId) {
if (shouldUseNewService()) {
return newUserService.getUser(userId);
}
return legacyUserService.getUser(userId);
}
private boolean shouldUseNewService() {
if (!newServiceEnabled) return false;
return ThreadLocalRandom.current()
.nextInt(100) < rolloutPercentage;
}
}
Rollout Plan:
- Week 1: 5% traffic
- Week 2: 10% traffic
- Week 3: 25% traffic
- Week 4: 50% traffic
- Week 5: 100% traffic
Phase 5: Data Synchronization
@Component
public class DataSyncScheduler {
@Scheduled(cron = "0 */5 * * * *") // Every 5 minutes
public void syncUsers() {
List<User> legacyUsers = legacyDb.getModifiedSince(
lastSyncTime
);
legacyUsers.forEach(user -> {
try {
newDb.upsert(user);
} catch (Exception e) {
log.error("Sync failed for user: " + user.getId(), e);
alertService.notify(e);
}
});
}
}
Challenges & Solutions
Challenge 1: Inconsistent Data
Problem: Legacy database had inconsistent data and no foreign key constraints.
Solution:
@Service
public class DataCleanupService {
public void cleanAndMigrate(User legacyUser) {
User cleanUser = User.builder()
.id(legacyUser.getId())
.email(sanitize(legacyUser.getEmail()))
.name(validateName(legacyUser.getName()))
.status(normalizeStatus(legacyUser.getStatus()))
.build();
newUserRepository.save(cleanUser);
}
}
Challenge 2: Session Management
Problem: PHP sessions stored in Redis with custom serialization.
Solution: Built a session bridge service
@Service
public class SessionBridge {
public Session convertPhpSession(String phpSessionId) {
String phpData = redisTemplate
.opsForValue()
.get("PHPSESSID:" + phpSessionId);
Map<String, Object> data = phpSerializer
.deserialize(phpData);
return Session.builder()
.userId(data.get("user_id"))
.permissions(data.get("permissions"))
.build();
}
}
Challenge 3: Zero Downtime Deployment
Solution: Blue-Green Deployment
# Deploy new version (green)
kubectl apply -f deployment-v2.yaml
# Wait for health checks
kubectl wait --for=condition=ready pod -l version=v2
# Switch traffic
kubectl patch service user-service \
-p '{"spec":{"selector":{"version":"v2"}}}'
# Keep v1 for rollback (15 minutes)
sleep 900
# Cleanup old version
kubectl delete deployment user-service-v1
Results
After complete migration:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Deployment Time | 2-3 hours | 15 minutes | 92% faster |
| Average Response Time | 800ms | 150ms | 81% faster |
| P95 Response Time | 3000ms | 400ms | 87% faster |
| Deployments/Month | 2 | 80+ | 40x more |
| Infrastructure Cost | $50K/month | $35K/month | 30% savings |
| Incident Recovery | 4 hours | 20 minutes | 92% faster |
Key Learnings
1. Start Small
Begin with services that have clear boundaries and minimal dependencies.
2. Invest in Observability
You can't migrate what you can't measure.
@Aspect
@Component
public class MetricsAspect {
@Around("@annotation(Timed)")
public Object measureTime(ProceedingJoinPoint pjp) {
Timer.Sample sample = Timer.start(registry);
try {
return pjp.proceed();
} finally {
sample.stop(Timer.builder("method.timed")
.tag("class", pjp.getTarget().getClass().getSimpleName())
.tag("method", pjp.getSignature().getName())
.register(registry));
}
}
}
3. Maintain Feature Parity
Don't add new features during migration. Focus on parity first.
4. Database Migration is the Hardest Part
Plan for dual writes, data consistency checks, and rollback strategies.
5. Team Training
Invest in Spring Boot training for the PHP team.
Tech Stack
New Architecture:
- Spring Boot 2.7
- Spring Cloud (Eureka, Config Server)
- PostgreSQL
- Redis
- Kafka
- Kubernetes
- Docker
- AWS (EKS, RDS, ElastiCache)
Tools:
- GitLab CI/CD
- Terraform
- Helm
- Prometheus & Grafana
- ELK Stack
Timeline & Team
- Duration: 12 months
- Team Size: 8 engineers
- Budget: $1.2M
- ROI: Positive within 18 months
Conclusion
Migrating from monolith to microservices is a marathon, not a sprint. The key is:
- Have a clear migration strategy
- Invest in infrastructure and tooling
- Migrate gradually with rollback capabilities
- Monitor everything
- Keep the business running
The result? A scalable, maintainable platform that enabled Rakuten Beauty to launch features 10x faster and scale to handle 3x traffic.
Want to discuss microservices migration strategies? Let's connect on LinkedIn!

