Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.1.5] - 2026-02-07

### Changed
- Version bump for next development cycle

## [2.1.4] - 2026-02-01

### Changed
Expand Down
8 changes: 7 additions & 1 deletion CODING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: "Revolutionary technology for writing deterministic, AI-friendly, h

# Java Backend Coding Technology: Writing Code in the Era of AI

**Version:** 2.1.4 | **Repository:** [github.com/siy/coding-technology](https://github.com/siy/coding-technology) | **Changelog:** [CHANGELOG.md](https://github.com/siy/coding-technology/blob/main/CHANGELOG.md)
**Version:** 2.1.5 | **Repository:** [github.com/siy/coding-technology](https://github.com/siy/coding-technology) | **Changelog:** [CHANGELOG.md](https://github.com/siy/coding-technology/blob/main/CHANGELOG.md)

## Introduction: Code in a New Era

Expand Down Expand Up @@ -2351,6 +2351,12 @@ public interface ProcessOrder {

Four steps, each a single-method interface. The `execute()` body reads top-to-bottom: validate → reserve → process payment → confirm. Each step returns `Result<T>`, so we chain with `flatMap`. If any step fails, the chain short-circuits and returns the failure.

**Why interface + factory?** Every component — use case, step, adapter — is defined as an interface with a static factory method. This is not arbitrary convention:

- **Substitutability**: Anyone can implement the interface. Testing, stubbing incomplete implementations, swapping adapters — all work without framework magic or inheritance hierarchies.
- **Implementation isolation**: Each implementation is self-contained. No shared base classes, no abstract methods to override, no coupling between implementations. Each intersection between implementations is unnecessary coupling with corresponding maintenance overhead — up to needing deep understanding of two projects instead of one, with zero benefit.
- **Disposable implementation**: A local record or lambda returned by the factory can't be referenced externally. The implementation is replaceable by definition. The interface is the design artifact; the implementation is incidental.

Async example (same structure, different types):
```java
public Promise<Response> execute(Request request) {
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Java Backend Coding Technology

> **Version 2.1.4** | [Full Changelog](CHANGELOG.md)
> **Version 2.1.5** | [Full Changelog](CHANGELOG.md)

A framework-agnostic methodology for writing predictable, testable Java backend code optimized for human-AI collaboration.

Expand Down Expand Up @@ -231,7 +231,7 @@ void email_acceptsValidFormat() {
### Changelog & Versioning

- **[CHANGELOG.md](CHANGELOG.md)** - Version history following [Keep a Changelog](https://keepachangelog.com/)
- Current version: 2.1.4 (2026-01-19)
- Current version: 2.1.5 (2026-02-07)
- Golden formatting patterns, Pragmatica Lite 0.11.2, 36 lint rules
- Semantic versioning for documentation releases

Expand Down Expand Up @@ -392,6 +392,6 @@ If you find this useful, consider [sponsoring](https://github.com/sponsors/siy).

---

**Version:** 2.1.4 | **Last Updated:** 2026-01-19 | **[Full Changelog](CHANGELOG.md)**
**Version:** 2.1.5 | **Last Updated:** 2026-02-07 | **[Full Changelog](CHANGELOG.md)**

**Copyright © 2025 Sergiy Yevtushenko. Released under the [MIT License](LICENSE).**
4 changes: 2 additions & 2 deletions ai-tools/agents/jbct-coder.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: jbct-coder
title: Java Backend Coding Technology Agent
description: Specialized agent for generating business logic code using Java Backend Coding Technology v2.1.4 with Pragmatica Lite Core 0.11.2. Produces deterministic, AI-friendly code that matches human-written code structurally and stylistically. Includes evolutionary testing strategy guidance.
description: Specialized agent for generating business logic code using Java Backend Coding Technology v2.1.5 with Pragmatica Lite Core 0.11.2. Produces deterministic, AI-friendly code that matches human-written code structurally and stylistically. Includes evolutionary testing strategy guidance.
tools: Read, Write, Edit, MultiEdit, Grep, Glob, LS, Bash, TodoWrite, Task, WebSearch, WebFetch
---

Expand Down Expand Up @@ -2018,7 +2018,7 @@ public class JooqUserRepository implements SaveUser {

## References

- **Full Guide**: `CODING_GUIDE.md` - Comprehensive explanation of all patterns and principles (v2.1.4)
- **Full Guide**: `CODING_GUIDE.md` - Comprehensive explanation of all patterns and principles (v2.1.5)
- **Testing Strategy**: `series/part-05-testing-strategy.md` - Evolutionary testing approach, integration-first philosophy, test organization
- **Systematic Application**: `series/part-10-systematic-application.md` - Checkpoints for coding and review
- **API Reference**: `CLAUDE.md` - Complete Pragmatica Lite API documentation
Expand Down
2 changes: 1 addition & 1 deletion ai-tools/skills/jbct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Claude Code navigates to specific files as needed, optimizing context usage.

## Version

Based on Java Backend Coding Technology v2.1.4
Based on Java Backend Coding Technology v2.1.5

## License

Expand Down
188 changes: 188 additions & 0 deletions articles/interface-factory-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: "Why Interface + Factory? The Java Pattern That Makes Everything Replaceable"
tags: [java, architecture, design-patterns, functional-programming]
canonical_url: https://pragmatica.dev/articles/interface-factory-pattern
published: true
description: "Interfaces with static factory methods aren't just convention. They make implementations disposable, testing trivial, and coupling impossible."
---

# Why Interface + Factory? The Java Pattern That Makes Everything Replaceable

## The Pattern

Every component — use case, processing step, adapter — is defined as an interface with a static factory method:

```java
public interface ProcessOrder {
record Request(String orderId, String paymentToken) {}
record Response(OrderConfirmation confirmation) {}

Result<Response> execute(Request request);

interface ValidateInput {
Result<ValidRequest> apply(Request raw);
}
interface ReserveInventory {
Result<Reservation> apply(ValidRequest req);
}
interface ProcessPayment {
Result<Payment> apply(Reservation reservation);
}
interface ConfirmOrder {
Result<Response> apply(Payment payment);
}

static ProcessOrder processOrder(ValidateInput validate,
ReserveInventory reserve,
ProcessPayment processPayment,
ConfirmOrder confirm) {
return request -> validate.apply(request)
.flatMap(reserve::apply)
.flatMap(processPayment::apply)
.flatMap(confirm::apply);
}
}
```

Four steps. Each is a single-method interface. The factory method accepts all dependencies as parameters and returns a lambda implementing the use case. The body reads exactly like the business process: validate, reserve, process payment, confirm.

This isn't arbitrary convention. There are three specific reasons this structure exists.

## Reason 1: Substitutability Without Magic

Anyone can implement the interface. No framework. No inheritance hierarchy. No annotations.

Testing becomes trivial:

```java
@Test
void order_fails_when_inventory_insufficient() {
var useCase = ProcessOrder.processOrder(
request -> Result.success(new ValidRequest(request)), // always valid
req -> INSUFFICIENT_INVENTORY.result(), // always fails
reservation -> { throw new AssertionError("unreachable"); },
payment -> { throw new AssertionError("unreachable"); }
);

useCase.execute(new Request("order-1", "tok_123"))
.onSuccess(Assertions::fail);
}
```

No mocking framework. No `@Mock` annotations. No `when().thenReturn()` chains. The test constructs the exact scenario it needs with plain lambdas.

Stubbing incomplete implementations during development is equally straightforward:

```java
// Payment gateway isn't ready yet? Stub it.
var useCase = ProcessOrder.processOrder(
realValidator,
realInventoryService,
reservation -> Result.success(new Payment("stub-" + reservation.id(), Money.ZERO)),
realConfirmation
);
```

The team working on inventory doesn't need to wait for the payment team. Each step is independently implementable.

## Reason 2: Implementation Isolation

Each implementation is self-contained. No shared base classes. No abstract methods to override. No coupling between implementations whatsoever.

Compare with the typical abstract class approach:

```java
// The abstract class trap
public abstract class AbstractOrderProcessor {
protected final Logger log = LoggerFactory.getLogger(getClass());

public final Result<Response> execute(Request request) {
log.info("Processing order: {}", request.orderId());
var result = doExecute(request);
log.info("Order result: {}", result);
return result;
}

protected abstract Result<Response> doExecute(Request request);
protected abstract Result<ValidRequest> validate(Request request);

// "Shared utility" that every subclass now depends on
protected Result<Money> calculateTotal(List<LineItem> items) {
// 47 lines of logic that one subclass needed once
}
}
```

Now every implementation is coupled to the base class. Change `calculateTotal` and you need to understand every subclass. Add logging to `execute` and every implementation gets it whether appropriate or not. The base class becomes a gravity well — accumulating shared code that creates invisible dependencies between implementations that should have nothing in common.

With interface + factory, there is no shared implementation code. Period. Each intersection between implementations is unnecessary coupling with corresponding maintenance overhead — up to needing deep understanding of two projects instead of one, with zero benefit.

```java
// Implementation A: uses database
static ProcessPayment databasePayment(PaymentRepository repo) {
return reservation -> repo.charge(reservation.paymentToken(), reservation.total())
.map(Payment::fromRecord);
}

// Implementation B: uses external API
static ProcessPayment stripePayment(StripeClient client) {
return reservation -> client.createCharge(reservation.total(), reservation.paymentToken())
.map(Payment::fromStripe);
}
```

These implementations don't know about each other. They don't share code. They don't share a base class. They share a contract — the interface — and nothing else.

## Reason 3: Disposable Implementation

Here's the subtle one. The factory method returns a lambda or local record. It can't be referenced externally by class name.

```java
static ProcessOrder processOrder(ValidateInput validate,
ReserveInventory reserve,
ProcessPayment processPayment,
ConfirmOrder confirm) {
return request -> validate.apply(request) // this lambda IS the implementation
.flatMap(reserve::apply)
.flatMap(processPayment::apply)
.flatMap(confirm::apply);
}
```

No code anywhere says `new ProcessOrderImpl()`. No code depends on the implementation class. The implementation is replaceable by definition — because nothing can reference it.

The interface is the design artifact. The implementation is incidental.

This might sound academic until you need to:
- Replace a synchronous implementation with an async one
- Swap a database adapter for an API adapter
- Add a caching layer around an existing step
- Completely rewrite a step's internals

In each case, the interface stays. The factory method signature stays. The implementation — which nothing references — gets replaced. No downstream changes. No adapter layers. No "backwards compatibility."

## The Compound Effect

Each reason is valuable on its own. Together, they create a system where:

**Testing is configuration.** You assemble the exact combination of real and stubbed components you need. No mocking framework overhead. No "mock everything" test fragility.

**Refactoring is safe.** Replacing an implementation can't break other implementations because they don't share code. The compiler enforces the contract through the interface.

**Complexity is bounded.** Understanding one implementation requires understanding only that implementation and the interfaces it consumes. Not a base class hierarchy. Not shared utilities. Not other implementations.

**Incremental development is natural.** Stub what's not ready. Replace stubs with real implementations one at a time. Each step can be developed, tested, and deployed independently.

## When Does This Not Apply?

When there genuinely is one implementation and always will be. Pure utility functions, mathematical computations, simple data transformations — these don't need the interface + factory treatment. A static method is fine.

The pattern pays for itself when there's any possibility of multiple implementations — including the test implementation, which almost always exists.

## The Shift

Most Java codebases default to classes. Interface extraction happens later, reluctantly, when testing forces it or when the second implementation appears.

Flip this. Start with the interface. Define the contract. The implementation follows naturally — and when it needs to change, nothing else does.

The interface is what you design. The implementation is what you happen to write today.
45 changes: 45 additions & 0 deletions articles/jbct-aether-series/00-aether-let-java-be-java.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Pragmatica Aether: Let Java Be Java

We're building java applications like any other in any other language. Assembling fat jars, building docker images, deploying in kubernetes. This looks normal,
everyone does it. We're ignoring the fact, that this approach contradicts the basic principles of software engineering: proper abstraction layering and
separation of concerns. We're wearing each application into a heavy winter coat, arming it to the teeth to make it survive in the hostile environment. Why not just make
environment more friendly instead? Let the application handle business logic. Let the environment handle infrastructure. That's what Aether is all about.

## Pragmatica Aether: Let Java Be Java
The core idea of Aether is to let application be focused on that matters: business logic. Scaling, unreliable connectivity, service discovery, retries, circuit breakers, configuration,
observability, logging, tracing, monitoring, security, etc., etc. are not application concerns and should not be handled at business logic level.

As a side effect, Aether makes unnecessary fat application frameworks. You still "deep" application, but not into heavy winter coat of Spring Boot, but into friendly
environment of Aether. This immediately gives a number of benefits:
- Clean separation between business logic and infrastructure -> independent maintenance, simple testing, (TODO: more advantages)
- Instant deployment readiness: mvn package -> application is ready to run, no need to wait for docker image build
- Transparent scaling
- Consistent configuration, logging, tracing, observability
- Simple mental model: method call is just a method call
- No frameworks. No magic. No hidden complexity or processing/startup overhead.

## Aether: Closer Look
Aether is a fault-tolerant distributed runtime environment, capable of dynamic loading and execution of Java applications. The application consists of one or more components called "slices".
In traditional designs like modular monolith, each slice could be considered a service. It has a defined contract in the form of an interface. But, unlike traditional service, slice
is also a minimal unit of deployment and scaling. For convenience, entire application is defined as a blueprint - declarative configuration of slices that can be deployed as a whole.
There are no operational or complexity tradeoffs, slice could be as lean as single method. Actually this is the best recommended approach for the Aether applications. It also
means that your application can be scaled with per use case granularity - feature unfeasible for microservices due to enormous operational overhead.

From the infrastructure perspective, Aether is cluster of identical nodes. Since runtime and business logic are decoupled, application and node upgrades are independent of each other.
Fault-tolerant nature of Aether enables seamless upgrades and rollbacks without downtime. It also enables various advanced deployment scenarios and strategies like
transparent multi-cloud deployment, transition from on-premise to cloud and back, transition between cloud providers.

## Aether: Scaling Done Right
Aether design inherently supports two levels of horizontal scaling:
- Slices scaling: new slice instance is started at one of the already running nodes.
- Node scaling: new node is added to the cluster.

Aether has built-in configurable support for launching new nodes and replacing failed nodes. Fault tolerant nature makes it completely feasible to use cheaper
spot instances for additional scaling needs during peak loads. Using these features does not require PhD in distributed systems or team of DevOps engineers.

But that's not all. Aether uses layered scaling decision design. At the lowest level, Aether uses decision tree to make almost instant scaling decision based on metrics. The next level is a simple
built-in predictive scaler, which observes patterns and tries to predict future load before it happens. And at the highest level, there is a configurable LLM-based scaling and cluster health monitoring.
It tries to predict long-term load patterns and monitors cluster health to make scaling and maintenance decisions.



2 changes: 1 addition & 1 deletion book/TABLE_OF_CONTENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Java Backend Coding Technology
## Unified Code Through Functional Composition

**Based on:** JBCT v2.1.4 | **Pragmatica Lite Core:** 0.11.2
**Based on:** JBCT v2.1.5 | **Pragmatica Lite Core:** 0.11.2

---

Expand Down
2 changes: 1 addition & 1 deletion book/ch01-introduction.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Chapter 1: Introduction - Code Unification

**Based on:** JBCT v2.1.4 | **Pragmatica Lite Core:** 0.11.2
**Based on:** JBCT v2.1.5 | **Pragmatica Lite Core:** 0.11.2

## What You'll Learn

Expand Down
6 changes: 6 additions & 0 deletions book/ch08-advanced-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ public interface ProcessOrder {

Four steps, each a single-method interface. The `execute()` body reads top-to-bottom: validate -> reserve -> process payment -> confirm. Each step returns `Result<T>`, so we chain with `flatMap`. If any step fails, the chain short-circuits and returns the failure.

**Why interface + factory?** Every component — use case, step, adapter — is defined as an interface with a static factory method. This is not arbitrary convention:

- **Substitutability**: Anyone can implement the interface. Testing, stubbing incomplete implementations, swapping adapters — all work without framework magic or inheritance hierarchies.
- **Implementation isolation**: Each implementation is self-contained. No shared base classes, no abstract methods to override, no coupling between implementations. Each intersection between implementations is unnecessary coupling with corresponding maintenance overhead — up to needing deep understanding of two projects instead of one, with zero benefit.
- **Disposable implementation**: A local record or lambda returned by the factory can't be referenced externally. The implementation is replaceable by definition. The interface is the design artifact; the implementation is incidental.

### Async Example

Same structure, different types:
Expand Down
Loading