SOAP or REST? PHP or Java? Single container or multiple containers?
These technical debates have ignited conference rooms for two decades. Developers divide, architects clash, CTOs must decide. But ultimately, this debate is a trap.
The real question is infinitely simpler, and it's often forgotten:
Are your layers separated and protected from each other?
That's the only question that truly matters for your project's longevity, your ability to evolve without fear, and long-term maintainability.
The Real Question Isn't Technological
These technical debates have ignited conference rooms for two decades. Developers divide, architects clash, CTOs must decide. But ultimately, this debate is a trap.
The real question is infinitely simpler, and it's often forgotten:
Are your layers separated and protected from each other?
That's the only question that truly matters for your project's longevity, your ability to evolve without fear, and long-term maintainability.
The Problem: Architectural Coupling
What I've Seen Too Often
I've had the opportunity to audit dozens of projects, from startup stage to large enterprises. The pattern that comes up most often? Everything is in the same stack.
Here's what that means concretely:
-
A protocol change = a complete overhaul. Migrating from SOAP to REST? You have to modify all your dependencies, tests, and deployment. Regression risks explode.
-
A technology update = a forced migration everywhere. Your language ages, you want to jump a major version, and you discover every corner of the project depends on it. Welcome to circular dependency hell.
-
Technology lock-in. You're a prisoner of your initial choices. Changing databases, adding caching, modifying communication formats? It's like changing a house's foundation—everything risks collapsing.
The Real Cost
This coupling isn't just a technical annoyance. It kills productivity:
- Change duration multiplied by 3-5x. A simple feature that should take 2 days takes 10 because of dependencies.
- Constant regression risk. Nobody dares touch legacy code, bugs accumulate.
- Talent fleeing out the door. Good devs want to work on clean projects, not ones where "everything depends on everything."
The Turning Point: A Banking Lesson
More Than 10 Years Ago at a Major Bank
I had the chance to work in a large French banking organization at a time when critical systems had zero margin for error. No rapid deployment, no "move fast and break things"—just an inescapable architectural discipline.
And that's where I learned something I've kept with me ever since.
The Simple Principle That Changes Everything
Strictly isolate your layers. No compromises, no exceptions.
Each layer has:
- A single, clearly defined responsibility (no mixing concerns)
- An explicit contract (an interface, a signature, rules of engagement)
- Isolated implementation (whatever language or protocol used doesn't matter)
The magic? As long as the contract holds, nothing else matters.
Why It's Powerful
It became my guiding principle. And I've never abandoned it because I've seen, again and again, how it saves projects.
A layer can change completely → other layers don't know about it. A bug is confined to one layer → it doesn't contaminate the rest. Evolution is predictable → you can plan progressive migrations.
The Solution: Docker as an Isolation Tool
Docker Democratized What Was Reserved for Experts
Docker didn't invent layer isolation. That principle has existed since the 1970s with the OSI model. But here's what Docker changed:
Before Docker, isolation was mostly enforced by code itself. You had to be disciplined, document your interfaces, ensure code reviews. One undisciplined dev could ruin the architecture.
After Docker, you can enforce isolation at the infrastructure level.
How Docker Forces Isolation
Each container:
- Runs in its own process space → no direct access to another layer's global variables
- Has its own filesystem → no hidden dependencies
- Communicates only via ports → contracts are explicit and networked
- Can be restarted independently → failures are isolated
This is isolation enforced by infrastructure, not just by discipline.
The Concrete Result
Today, I apply this principle everywhere:
-
SOAP becomes REST? OK, the contract stays the same. I change the service's implementation, redeploy its container, and other services see no difference.
-
PHP backend becomes Java Spring Boot? OK, the API doesn't change. I rewrite the entire code in another language, and the rest of the system continues peacefully.
-
Add caching (Redis)? OK, it's transparent to other layers. I wire up Redis, improve performance, and nobody else has to do anything.
Concrete Examples with Dockerfile
Example 1: Isolated REST API Service
Here's a simple API layer, isolated in its container:
FROM php:8.2-fpm
WORKDIR /app
# Dependencies isolated to this container
COPY composer.json composer.lock ./
RUN apt-get update && apt-get install -y git unzip \
&& composer install --no-dev --prefer-dist \
&& apt-get clean
# Application code
COPY src/ ./src
COPY config/ ./config
# Healthcheck to verify the layer is working
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9000/health || exit 1
EXPOSE 9000
CMD ["php-fpm"]
Key point: This layer exposes ONE contract (the API on port 9000). Whatever's inside doesn't matter to other services.
Example 2: Isolated Cache Service
FROM redis:7-alpine
WORKDIR /data
# Isolated configuration
COPY redis.conf .
# Persistence limited to this container
VOLUME ["/data"]
EXPOSE 6379
CMD ["redis-server", "redis.conf"]
Key point: Redis is completely isolated. The API can be restarted without touching the cache. The cache can be reset without breaking the API.
Example 3: Orchestration with docker-compose
version: '3.8'
services:
# API layer
api:
build: ./api
ports:
- "8000:9000"
environment:
- CACHE_HOST=cache
- DB_HOST=database
depends_on:
- cache
- database
networks:
- backend
# Cache layer
cache:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- backend
volumes:
- cache_data:/data
# Database layer
database:
image: postgres:15
environment:
- POSTGRES_DB=app
- POSTGRES_PASSWORD=secret
networks:
- backend
volumes:
- db_data:/var/lib/postgresql/data
networks:
backend:
driver: bridge
volumes:
cache_data:
db_data:
Key point: Each service communicates via its defined port. Environment variables specify contracts. If you replace api with a Java implementation, you just change the build—everything else keeps working.
Use Cases and Measurable Gains
Real Case: Event Payment System
A few years ago, I implemented an isolated architecture for a Point of Sale system used at public events (village fairs, flea markets, small markets).
Initial setup: everything was coupled. Monolithic PHP backend, integrated MySQL database, file-based sessions.
The problem: at events with traffic spikes (100+ transactions/minute), bandwidth exploded. Sessions were lost, requests were retried 3-4 times, customers waited.
The solution: isolate the layers.
- API layer: reduce to bare essentials (just business logic)
- Cache layer: Redis for sessions (200x faster than files)
- Database layer: optimized PostgreSQL, physically separate
The result: bandwidth consumption was divided by 4. For the same load, we used 25% of the bandwidth. Timeouts disappeared. UX improved dramatically.
Other Observed Gains
For CTOs modernizing legacy systems:
- Progressive migrations become possible. Migrate one layer at a time, validate, deploy. No risky big bang.
For freelancers:
- Projects remain maintainable long-term. You can return to a project 2 years later, change a layer, and stay confident.
For senior developers:
- Clear, defensible architecture. No emotional debates about "PHP vs Java." It's just a container, as long as it respects the contract.
Isolation Best Practices
1. Define a Clear Contract for Each Layer
Each layer should be documented:
- What: what does this layer do?
- Interface: what port, protocol, data format?
- Responsibilities: what it does AND what it doesn't do
- Dependencies: what does it need to function?
2. Use Environment Variables for Dependencies
No hardcoding addresses. Use env vars:
ENV DB_HOST=${DB_HOST:-localhost}
ENV CACHE_HOST=${CACHE_HOST:-localhost}
ENV LOG_LEVEL=${LOG_LEVEL:-info}
This lets you change configuration without touching code.
3. Isolate Data Volumes
Each layer needing persistence should have its own volume:
volumes:
api_logs: # Logs isolated to API
cache_data: # Data isolated to cache
db_data: # Data isolated to database
Never a shared "everything in here" volume that creates coupling.
4. Manage Dependencies with depends_on and Healthchecks
api:
depends_on:
database:
condition: service_healthy
This enforces startup order AND ensures each layer is actually ready.
5. Structured Logging, No Cross-Layer Log Dependencies
Each container logs to STDOUT. A centralized system (ELK, Grafana Loki) aggregates. No dependencies between each layer's logs.
6. Test Isolation
Run independence tests:
- Restart one layer in production → nothing else should fail
- Change one layer's language → other services should work without modification
Conclusion
The Real Power of Isolation
Docker didn't invent multi-layer architecture. But it made it accessible to every developer, every team, every budget.
You're out of excuses for keeping coupled code.
Isolation isn't a luxury. It's a freedom.
The freedom to evolve without fear. The freedom to change implementation without breaking everything. The freedom to do progressive migrations. The freedom to stay focused on what matters: business value.
Three Perspectives
-
For the CTO modernizing legacy systems: this approach lets you migrate progressively without risky big bangs.
-
For the freelancer: it makes your projects maintainable long-term, increases your perceived value, lets you return to old code with confidence.
-
For the senior developer: it spares you the headaches of "everything depends on everything." You can make technical choices without fearing side effects.
Next Steps
-
Audit one of your projects: identify couplings. Where is "everything depends on everything"?
-
Refactor one layer: extract a responsibility, put it in a container, define its clear contract.
-
Measure the impact: before/after. Deployment time, regression risk, maintainability. The numbers speak.
Have you lived through this coupling? How did you solve it?
Share your real-world experience. The best solutions come from field feedback.