MigrationMicroservicesSpring BootPHP

Microservices Migration: PHP to Java Spring Boot at Rakuten

January 5, 2025
5 min read
Share:
Microservices Migration: PHP to Java Spring Boot at Rakuten

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

  1. Scalability: Couldn't scale specific features independently
  2. Deployment: High-risk, infrequent deployments
  3. Performance: Slow response times during peak hours
  4. Development Speed: Features took months to ship
  5. 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:

MetricBeforeAfterImprovement
Deployment Time2-3 hours15 minutes92% faster
Average Response Time800ms150ms81% faster
P95 Response Time3000ms400ms87% faster
Deployments/Month280+40x more
Infrastructure Cost$50K/month$35K/month30% savings
Incident Recovery4 hours20 minutes92% 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:

  1. Have a clear migration strategy
  2. Invest in infrastructure and tooling
  3. Migrate gradually with rollback capabilities
  4. Monitor everything
  5. 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!

Shahariar Hossen

Shahariar Hossen

Senior Full Stack Engineer with 6+ years of experience in building scalable systems. Specialist in Spring Boot, Microservices, and AI/ML.

Continue Reading