Skip to main content

Chapters 30–32: The Database, Web & Frameworks Are Details

"The database is a detail. The web is a detail. The framework is a detail."

🎓 For New Learners

The Central Claim

These three chapters share one thesis: none of the things developers spend the most time configuring are architecturally central.

Databases, web frameworks, Spring — they are implementation choices hidden behind boundaries so the business rules at the center never need to know about them.

Chapter 30: The Database Is a Detail

The data model belongs to the domain. The database is a performance optimization for storage. Treating it as the system's center means:

  • Business rules are shaped by table schemas
  • Changing databases requires rewriting business logic
  • Testing requires a running database, making tests slow

The fix — hide storage behind an interface:

// Domain contract — the business rule's view of persistence
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
}

// Infrastructure detail — JPA implementation
@Repository
public class JpaOrderRepository implements OrderRepository {
public Optional<Order> findById(OrderId id) {
return springRepo.findById(id.value()).map(OrderJpaMapper::toDomain);
}
}

// Infrastructure detail — MongoDB implementation (swap without touching use cases)
@Repository
public class MongoOrderRepository implements OrderRepository { ... }

The use case works with either. The database decision is deferred and swappable.

The thought experiment: if there were no disk, you'd build in memory. Everything would work. Persistence would be added as a plugin at the end — not woven throughout. That's the correct relationship: business logic is the system; the database is a plugin.

Chapter 31: The Web Is a Detail

HTTP is an I/O device — a mechanism to move data in and out. Business rules should know nothing about HTTP, REST conventions, JSON, or URL structure.

// Use case: has NO idea it's being called over HTTP
public class PlaceOrderUseCase {
public PlaceOrderResult execute(PlaceOrderCommand cmd) { ... }
// Could be called from REST, gRPC, CLI, tests, message consumers — identical
}

// HTTP detail isolated to the controller adapter
@RestController
public class PlaceOrderController {
@PostMapping("/orders")
public ResponseEntity<PlaceOrderResponse> handle(@RequestBody PlaceOrderRequest req) {
var result = placeOrderUseCase.execute(PlaceOrderCommandMapper.from(req));
return ResponseEntity.ok(PlaceOrderResponse.from(result));
}
}

Adding a gRPC endpoint means adding a new adapter — not touching the use case.

Chapter 32: Frameworks Are Details

When you adopt a framework, the framework authors ask you to commit to them — inherit their classes, annotate with their annotations, structure code around their conventions.

The Rule: frameworks belong in Ring 3 (adapters) and Ring 4 (infrastructure). They must never appear in Ring 1 (entities) or Ring 2 (use cases).

✅ Spring annotations: RestController, Repository, Configuration — in adapters/infrastructure
❌ Spring annotations: @Transactional, @Entity, @Autowired — in domain entities/use cases

🔬 Senior Deep Dive

The Spring Coupling Tax

Spring is excellent, but its annotations carry a coupling tax:

// All of this is Spring — the one line of business logic is buried
@RestController @RequestMapping("/orders") @Validated
public class OrderController {
@Autowired private OrderService svc;

@PostMapping @Transactional
public @ResponseBody ResponseEntity<?> placeOrder(
@RequestBody @Valid OrderRequest req) {
// one line of actual business logic
return ResponseEntity.ok(svc.place(req));
}
}

Nothing wrong here — it's a controller adapter, Spring belongs here. The problem is when this pattern bleeds into domain entities and use cases.

What a clean domain entity looks like:

// Zero Spring imports — pure Java business rule
public class Order {
private final OrderId id;
private final List<LineItem> items;
private OrderStatus status;

public void place() {
if (items.isEmpty()) throw new EmptyOrderException();
this.status = OrderStatus.PLACED;
}

public Money calculateTotal() {
return items.stream()
.map(LineItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
}

Framework Replacement Realism

ReplacementCost (clean architecture)Cost (framework-coupled)
MySQL → PostgreSQL1 day (infrastructure only)Weeks (business logic changes)
Spring MVC → WebFluxDays (controller adapters only)Months (everything mixed together)
RabbitMQ → KafkaDays (adapter reimplementation)Weeks (event handling scattered)
Full framework swapWeeksYears / rewrite

The architectural payoff is not that you will replace frameworks — it's that you could, at reasonable cost.

Testing as the Architectural Litmus Test

The fastest signal that a framework has leaked into the wrong layer:

// Red flag: testing business logic requires Spring context
@SpringBootTest // ← this should never be needed for pure domain tests
class OrderTest {
@Autowired Order order; // ← domain objects should never need injection

@Test void orderCanBePlaced() { ... }
}

// Green flag: domain tests need nothing
class OrderTest {
@Test void orderCanBePlaced() {
var order = Order.create(customerId, items); // just Java
order.place();
assertThat(order.getStatus()).isEqualTo(OrderStatus.PLACED);
}
}

If your domain tests require @SpringBootTest, your domain has been contaminated by a detail. The boundary is missing.


Summary

DetailThe MistakeThe Fix
DatabaseTreating table schema as the domain modelRepository interface in domain; ORM mapping in infrastructure
Web/HTTPUse cases that parse HTTP requestsThin controller adapters; use cases receive plain command objects
FrameworksSpring annotations in domain entitiesFramework annotations only in adapter/infrastructure rings
The test signalTests that need the framework to test business logicBusiness logic testable with plain JUnit — no Spring context needed