---
title: 'Data Access Abstraction'
source: 'https://academia.sh/en/courses/enterprise-application-framework/data-access-abstraction'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that the same compiled set of classes calling the Repository interface runs against two separate implementations without ever being recompiled'
    - "Confirms that the caller's class file mentions only the Repository interface, no implementation class at all"
    - "Measures that the interface's promise and the implementation's behavior are separate, by showing that the same call runs without a hitch under one implementation while throwing an exception under the other"
---

# Data Access Abstraction

The same compiled set of callers — the object container, the service, and the entry point — runs against two separate implementations of a repository interface, without ever being recompiled. The caller's class file mentions only the interface; the same call runs without a hitch under one implementation while falling with an exception under the other, because the interface's promise and the implementation's behavior are separate, and the container does not close this gap, it only hides it.

The previous lesson measured whether a request's data is correct, but it never asked where
that data gets written once it is processed. This lesson takes this up: it measures how the
container supplies the implementation of a **repository** interface, and how the same
caller can run against two separate implementations without ever being recompiled. The idea
of a repository — keeping business logic independent of persistence details — was
established in The Data Access Layer and Business Logic course and is not repeated here;
this lesson's question is separate from that: by what mechanism the container fills this
interface.

The database is modeled here too: no real connection is opened, no real SQL statement is
run. `Repository` is just an interface carrying two methods — reading a quantity, writing a
quantity — and what stands behind these two methods is invisible to the caller. The only
way to show how real this invisibility is, is to really run the same caller against two
separate implementations; the method used in this lesson is exactly that.

In the previous lessons, "running the same behavior two ways" meant comparing an object the
container built against a manually built object. This lesson carries the same principle one
step further: what is compared here is not two objects, but the same calling code run
against two separate **implementations**. This is also where an interface is truly tested —
imitating a single class's behavior is not enough; it also has to be seen whether a
completely different internal structure, holding the same contract, satisfies the same
calling code.

## The Same Caller, Two Separate Implementations

- **WD16.** This measurement stages a real classpath distinction: the interface, the
  calling code, and each implementation are compiled into separate directories, then
  brought together in different combinations at runtime. Which classpath a class has to be
  required in was measured in the Build, Testing and the Ecosystem course; here the same
  mechanism is used for the interface–implementation distinction, and the numbers from
  there are not repeated.
- **WD17.** The `Container.create` method takes the implementation class's name as a
  string parameter and finds it with `Class.forName`; this is how the caller can construct
  an instance without mentioning any implementation class at compile time.

The `Container`, `Service`, and `Main` classes are compiled only against the `Repository`
interface — neither `MemoryRepository` nor `ArrayRepository` ever enters the compile-time
classpath. These three classes are compiled exactly once, then run with two separate
runtime classpaths, never recompiled: with `MemoryRepository` in one, `ArrayRepository` in
the other. The only difference between the two is the third classpath component given to
the `run` call and the implementation name passed to `Main.main`.

```java
// TwoImplementations.java — the same compiled caller runs with two separate implementations without changing the interface
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

public class TwoImplementations {

    static Path write(Path directory, String name, String source) throws IOException {
        Files.createDirectories(directory);
        Path p = directory.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static void compile(Path output, List<Path> classpath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> arg = new ArrayList<>(List.of("-d", output.toString()));
        if (!classpath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classpath) sj.add(p.toString());
            arg.add("-classpath");
            arg.add(sj.toString());
        }
        for (Path k : sources) arg.add(k.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter outputText = new StringWriter();
        int result = compiler.run(new PrintWriter(outputText), new PrintWriter(outputText), arg.toArray(new String[0]));
        if (result != 0) throw new RuntimeException("compilation failed: " + outputText);
    }

    static String run(List<Path> classpath, String implementationName) {
        URL[] url = new URL[classpath.size()];
        try {
            for (int i = 0; i < url.length; i++) url[i] = classpath.get(i).toUri().toURL();
            try (URLClassLoader loader = new URLClassLoader(url, TwoImplementations.class.getClassLoader().getPlatformClassLoader())) {
                Class<?> main = Class.forName("Main", true, loader);
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                PrintStream old = System.out;
                System.setOut(new PrintStream(buffer));
                try {
                    main.getMethod("main", String[].class).invoke(null, (Object) new String[]{implementationName});
                } finally {
                    System.setOut(old);
                }
                return buffer.toString().strip();
            }
        } catch (InvocationTargetException e) {
            return "exception: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage();
        } catch (Throwable t) {
            return "exception: " + t.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("twoimpl");
        Path source = root.resolve("source");

        Path sInterface = write(source.resolve("interface"), "Repository",
                "public interface Repository {\n"
              + "  int findQuantity(String product);\n"
              + "  void updateQuantity(String product, int quantity);\n"
              + "}\n");

        Path sContainer = write(source.resolve("caller"), "Container",
                "public class Container {\n"
              + "  static Service create(String implementationName) throws ReflectiveOperationException {\n"
              + "    Class<?> c = Class.forName(implementationName);\n"
              + "    Repository repository = (Repository) c.getDeclaredConstructor().newInstance();\n"
              + "    return new Service(repository);\n"
              + "  }\n"
              + "}\n");
        Path sService = write(source.resolve("caller"), "Service",
                "public class Service {\n"
              + "  private final Repository repository;\n"
              + "  public Service(Repository repository) { this.repository = repository; }\n"
              + "  public String apply(String product, int change) {\n"
              + "    int current = repository.findQuantity(product);\n"
              + "    int updated = current + change;\n"
              + "    repository.updateQuantity(product, updated);\n"
              + "    return product + \": \" + current + \" -> \" + updated;\n"
              + "  }\n"
              + "}\n");
        Path sMain = write(source.resolve("caller"), "Main",
                "public class Main {\n"
              + "  public static void main(String[] args) throws Exception {\n"
              + "    Service service = Container.create(args[0]);\n"
              + "    System.out.println(service.apply(\"screw\", 5));\n"
              + "    System.out.println(service.apply(\"screw\", -2));\n"
              + "    System.out.println(service.apply(\"nail\", 3));\n"
              + "  }\n"
              + "}\n");

        Path sMemory = write(source.resolve("memory"), "MemoryRepository",
                "import java.util.*;\n"
              + "public class MemoryRepository implements Repository {\n"
              + "  private final Map<String, Integer> data = new HashMap<>();\n"
              + "  public int findQuantity(String product) { return data.getOrDefault(product, 0); }\n"
              + "  public void updateQuantity(String product, int quantity) { data.put(product, quantity); }\n"
              + "}\n");
        Path sArray = write(source.resolve("array"), "ArrayRepository",
                "public class ArrayRepository implements Repository {\n"
              + "  private final String[] names = new String[1];\n"
              + "  private final int[] quantities = new int[1];\n"
              + "  private int filled = 0;\n"
              + "  public int findQuantity(String product) {\n"
              + "    for (int i = 0; i < filled; i++) if (names[i].equals(product)) return quantities[i];\n"
              + "    return 0;\n"
              + "  }\n"
              + "  public void updateQuantity(String product, int quantity) {\n"
              + "    for (int i = 0; i < filled; i++) if (names[i].equals(product)) { quantities[i] = quantity; return; }\n"
              + "    if (filled == names.length) throw new IllegalStateException(\"repository capacity full: \" + product + \" could not be added\");\n"
              + "    names[filled] = product; quantities[filled] = quantity; filled++;\n"
              + "  }\n"
              + "}\n");

        Path cInterface = root.resolve("output/interface");
        Path cCaller = root.resolve("output/caller");
        Path cMemory = root.resolve("output/memory");
        Path cArray = root.resolve("output/array");

        compile(cInterface, List.of(), List.of(sInterface));
        compile(cCaller, List.of(cInterface), List.of(sContainer, sService, sMain));
        compile(cMemory, List.of(cInterface), List.of(sMemory));
        compile(cArray, List.of(cInterface), List.of(sArray));

        System.out.println("-- same caller (Container+Service+Main), with the MemoryRepository implementation --");
        System.out.println(run(List.of(cCaller, cInterface, cMemory), "MemoryRepository"));

        System.out.println("-- same caller, without ever recompiling, with the ArrayRepository implementation --");
        System.out.println(run(List.of(cCaller, cInterface, cArray), "ArrayRepository"));
        System.out.println("defect type: error (failed with the exception name, ArrayRepository rejected the call that exceeded its capacity)");
    }
}
```

```
-- same caller (Container+Service+Main), with the MemoryRepository implementation --
screw: 0 -> 5
screw: 5 -> 3
nail: 0 -> 3
-- same caller, without ever recompiling, with the ArrayRepository implementation --
exception: IllegalStateException: repository capacity full: nail could not be added
defect type: error (failed with the exception name, ArrayRepository rejected the call that exceeded its capacity)
```

Nowhere in `Service`'s source does the name `MemoryRepository` or `ArrayRepository` appear —
there is only a `Repository repository` field, and that field's type is the interface.
`Container.create` works the same way: the only information it receives is a string, and it
casts the class it finds with `Class.forName` to `Repository`. These three classes are
compiled in the first `compile` call, in an environment where no implementation is present
on the classpath, and their compiled form is reused across two separate `run` calls. The
only difference is the third component of the classpath list given to `run`.

What confirms that this compilation is genuinely "without recompiling" is that the
`compile(cCaller, ...)` call is made only once. The `cCaller` directory is produced once and
given to both `run` calls exactly as is; before the second `run` call, there is no line
anywhere that goes back to the `Container`, `Service`, or `Main` source and calls `compile`
again. In a real setup, this means an application's deployment package (the part holding the
caller classes) can be produced once, and which implementation gets attached is decided by
the runtime classpath alone — the same `.class` files can be bound to a different repository
with no change in the source code, running no recompilation step at all.

## The Interface's Promise and the Implementation's Behavior Are Separate

- **WD18.** `MemoryRepository` works on a `HashMap` and places no limit on how many
  distinct `product` names arrive. `ArrayRepository` uses two fixed-length arrays and can
  hold only one distinct `product` name.
- **WD19.** The `Repository` interface states this capacity difference nowhere; neither
  method's signature carries any promise about a limit.

In the first run, `screw` is updated twice (`0 -> 5`, then `5 -> 3`), and then a new record
is opened for a different product (`nail`) (`0 -> 3`). With `MemoryRepository`, all three
calls complete without a hitch. With `ArrayRepository`, the same three calls, in the same
order, are made through the same compiled `Service` class — but the third one falls with an
exception: `ArrayRepository` can hold only one distinct product name, and its capacity fills
once `nail` arrives as a second product name.

This is the lesson's bounding measurement: **the interface's promise and the
implementation's behavior are separate, and the container does not close this gap, it only
hides it.** The `Repository` interface nowhere promises that `updateQuantity` will always
succeed, but in practice, while working with `MemoryRepository`, this promise looks true
without ever being tested — since the limit is never exceeded, the problem never surfaces.
When the same code is bound to `ArrayRepository`, a capacity limit the interface never
promised suddenly becomes visible. The container hides from the caller which implementation
`Service` is working with, but it does not eliminate the behavioral difference between the
implementations — it only defers where that difference will surface, behind the interface,
to runtime.

There is also a reverse side to this distinction: both implementations give byte-for-byte
the same result for the two calls on the `screw` product. This is what the interface really
delivers — a read-write cycle done with a single key behaves the same regardless of
implementation. The problem arises only because an assumption the interface never states
(an unlimited number of distinct keys) holds under one implementation and fails under the
other. An interface promises the methods whose signatures it carries; everything not present
in the signature — capacity, concurrency, latency — is the implementation's own decision,
and the interface never carries it at all.

This also clarifies the container's real contribution in the data access layer. The
container decides which implementation will fill the `Service` class, but it nowhere
guarantees that this decision selected the **correct** implementation — `Container.create`
only learns, at runtime, that the string name it was given really points to a class
implementing `Repository`, and only as long as the cast at the line `(Repository)
c.getDeclaredConstructor().newInstance()` does not fail. Likewise, an implementation
violating an assumption the interface does not carry only surfaces once that assumption is
genuinely tested — a caller using a single product name with `ArrayRepository` would never
see the capacity limit. This lesson's third call carrying a different product name is a
deliberate choice: the limit only becomes visible once two distinct keys reach the same
repository, and this is exactly why testing an interface has to ask not only "is the method
being called" but also "where are the implementation's real limits."

## Summary

- The `Container`, `Service`, and `Main` classes were compiled once against only the
  `Repository` interface and run against two separate implementations
  (`MemoryRepository`, `ArrayRepository`) without ever being recompiled.
- No implementation class's name appears in the caller's class file; `Container.create`
  finds the implementation only by a string name, through reflection.
- The same three calls completed without a hitch under `MemoryRepository`; under
  `ArrayRepository`, the third call fell with an `IllegalStateException` — the same
  compiled code, the same calls in the same order, two separate outcomes.
- The `Repository` interface carries no guarantee, such as capacity, that is absent from
  its signature; differences like this surface only when the implementation changes.
- The container hides the implementation choice from the caller but does not eliminate the
  behavioral difference between implementations; it only defers that difference to
  runtime.

## Next Step

This lesson measured which implementation stands behind an interface, but it never asked
how the implementation's own fields match between an object and a repository. The next
lesson takes this up: it measures that the binding between an object's fields and a table's
columns comes from an annotation in the source, and that a field that is never accessed is
never loaded at all.
