Slices: The Right Size for Microservices
Source: Dev.to
The Granularity Trap
Every team that adopts microservices eventually hits the same wall: how big should a service be?
-
Go too small and you drown in network calls, distributed transactions, and deployment complexity.
- Your simple “get user profile” operation now involves five services, three of which are just proxies for database tables.
- Latency compounds.
- Debugging becomes archaeology.
-
Go too large and you’re back to the monolith.
- Different teams step on each other.
- Deployments require coordination.
- The “micro” in microservices becomes ironic.
The standard advice—“one service per bounded context” or “services should be independently deployable”—sounds reasonable but provides no actionable guidance.
- Where does one context end and another begin?
- What exactly makes something “independently deployable”?
Teams oscillate between extremes, refactoring services that are “too small” into larger ones, then splitting services that grew “too large.” The cycle repeats because the fundamental question remains unanswered: what determines the right boundary?
The problem isn’t size. It’s boundaries.
A well‑defined boundary has specific properties:
- Clear contract – callers know exactly what they can request and what they’ll receive.
- Explicit dependencies – the component declares what it needs from outside.
- Internal freedom – implementation details can change without affecting callers.
Size follows from boundaries, not the other way around. A component is the right size when it fully owns its boundaries—when everything needed to fulfill its contract lives inside, and everything outside is accessed through explicit dependencies.
Most microservice designs fail because they draw boundaries based on:
- Technical layers (API gateway, business logic, database access)
- Organizational structure (team ownership)
Neither approach produces stable boundaries because neither focuses on the actual contracts between components.
Introducing Slices
A slice is a deployable unit defined by its contract. You write an interface with a single annotation:
@Slice
public interface OrderService {
Promise createOrder(CreateOrderRequest request);
Promise getOrder(GetOrderRequest request);
Promise cancelOrder(CancelOrderRequest request);
}
That’s it. The annotation processor generates everything else—factory methods, dependency wiring, deployment metadata.
The interface is the boundary
- Methods define the contract – each takes a request and returns a
Promiseof a response. - Request/response types are explicit – no hidden parameters, no ambient context.
- Async by default –
Promisehandles both success and failure paths.
The implementation lives behind this interface. It might be simple or complex, call other slices, or be completely self‑contained. The boundary doesn’t care.
Slices run on Aether, a distributed runtime designed around slice contracts. You don’t configure service discovery, serialization, or inter‑slice communication—Aether handles it based on what the slice interfaces declare. Every inter‑slice call eventually succeeds if the cluster is alive; the runtime manages retries, failover, and recovery transparently.
Forge provides a development environment for testing slices under realistic conditions—load generation, chaos injection, backend simulation. Instead of deploying to staging to see how your slices behave under pressure, you run Forge locally and observe.
The development experience stays simple:
- Write
@Sliceinterfaces. - Implement them.
- Test with Forge.
- Deploy to Aether.
The annotation processor generates all the boilerplate—factories, dependency wiring, routing metadata.
Explicit Dependencies
Traditional service architectures bury dependencies in configuration files, environment variables, or runtime discovery. You discover what a service needs by reading its code, tracing its network calls, or waiting for it to fail in production.
Slices declare dependencies directly in the interface:
@Slice
public interface OrderService {
Promise createOrder(CreateOrderRequest request);
// Other methods...
// Factory method declares dependencies explicitly
static OrderService orderService(InventoryService inventory,
PaymentService payment) {
return OrderServiceFactory.orderService(Aspect.identity(),
inventory, payment);
}
}
The annotation processor generates the factory that wires everything:
public final class OrderServiceFactory {
private OrderServiceFactory() {}
public static OrderService orderService(
Aspect aspect,
InventoryService inventory,
PaymentService payment) {
return aspect.apply(new OrderServiceImpl(inventory, payment));
}
}
- The factory method signature declares dependencies.
- No service locators, no runtime discovery, no configuration files that might or might not match reality.
- Dependencies are visible at compile time and verified before deployment.
This explicitness matters:
- You can trace the dependency graph by reading code.
- You can test with substitutes by passing different implementations.
- Forge validates the entire graph before starting anything.
Three Runtime Modes (Same Slice Code)
| Mode | Description |
|---|---|
| Ember | Single‑process runtime with multiple cluster nodes. Fast startup, simple debugging. Ideal for local development. |
| Forge | Ember + load generation and chaos injection. Test how slices behave under pressure without deploying anywhere. |
| Aether | Full distributed cluster. Production deployment with all resilience guarantees. |
Your code doesn’t know which mode it’s running in. Slice interfaces, implementations, and dependencies stay identical. The runtime handles the difference—whether inter‑slice calls are in‑process or cross‑network is transparent.
Impact on the Development Workflow
- Write & debug in Ember – fast, in‑process execution.
- Stress‑test in Forge – inject load and failures.
- Deploy to Aether – production‑grade resilience.
At no point do you need to rewrite contracts, change configuration, or adjust code for the environment. The slice model lets you focus on boundaries, not on the size of services.
Slices and Their “Right Size”
Do you modify slice code to accommodate the environment?
With boundaries explicit and deployment flexible, the “right‑size” question dissolves.
When Is a Slice the Right Size?
- Interface – captures a coherent set of operations.
- Dependencies – accurately reflect what the slice actually needs.
- Implementation – can fulfill its contract.
There is no minimum or maximum.
- An authentication slice might expose two methods.
- An order‑processing slice might expose twenty.
Size follows from the domain, not from arbitrary rules about lines of code or team structure.
Recoverability
Getting a slice wrong is recoverable:
| Situation | What Happens |
|---|---|
| Split a slice that grew too complex | The boundary changes, but callers only see a new interface. |
| Merge slices that were artificially separated | Callers still see a single interface; the internal implementation is simply combined. |
Refactoring slices is just refactoring code, not rewriting infrastructure.
Slices as the Home of JBCT Patterns
Each slice method is a data‑transformation pipeline:
Parse input (validated request types)
↓
Gather data (dependencies, other slices)
↓
Process (business logic)
↓
Respond (typed response)
The six JBCT patterns—Leaf, Sequencer, Fork‑Join, Condition, Iteration, Aspects—compose within and across slice methods. A slice is simply the deployment boundary around a set of related transformations.
Why This Combination Works
- JBCT gives you a consistent structure inside slices.
- Slices give you consistent boundaries between slices.
Together they eliminate the two main sources of architectural entropy:
- Inconsistent implementation patterns
- Unclear service boundaries
From Service Granularity to Contract‑Centric Design
The micro‑services granularity problem persists because it asks the wrong question:
- Wrong question: “How big should a service be?” – no good answer.
- Right question: “What are the contracts between components?” – a precise answer.
Slices shift focus from size to boundaries:
- Define the interface.
- Declare dependencies explicitly.
- Let deployment topology adapt to operational needs rather than dictating code structure.
Result
- Boundaries that are clear by construction.
- Dependencies that are visible by design.
- Deployment flexibility that doesn’t require rewriting code.
Part of Java Backend Coding Technology – a methodology for writing predictable, testable backend code.