Skip to main content

Patterns: The multi-repo

Typr generates one repository per table. But often you need to coordinate multiple tables in a transaction.

Enter the multi-repo pattern: take low-level Typr repositories as parameters, and write the higher-level flow yourself.

Example​

MultiRepoExample
package showcase;

import java.sql.Connection;
import java.util.List;
import java.util.Optional;
import showcase.showcase.customer.*;
import showcase.showcase.address.*;
import showcase.showcase.customer_order.*;

/**
* Multi-repo pattern: coordinate multiple repositories in a single transaction.
* Typr generates low-level repos; you write higher-level business logic.
*/
public class MultiRepoExample {
private final CustomerRepo customerRepo;
private final AddressRepo addressRepo;
private final CustomerOrderRepo orderRepo;

public MultiRepoExample(
CustomerRepo customerRepo,
AddressRepo addressRepo,
CustomerOrderRepo orderRepo) {
this.customerRepo = customerRepo;
this.addressRepo = addressRepo;
this.orderRepo = orderRepo;
}

/** Create a customer with a shipping address and initial order */
public CustomerOrderRow createCustomerWithOrder(
CustomerRowUnsaved customer,
AddressRowUnsaved address,
CustomerOrderRowUnsaved order,
Connection c) {
// Insert customer first (no FK dependencies)
CustomerRow savedCustomer = customerRepo.insert(customer, c);

// Insert address (FK to customer)
AddressRowUnsaved addressWithCustomer = address.withCustomerId(savedCustomer.id());
AddressRow savedAddress = addressRepo.insert(addressWithCustomer, c);

// Insert order (FK to customer, optional FK to address)
CustomerOrderRowUnsaved orderWithRefs = order
.withCustomerId(savedCustomer.id())
.withShippingAddressId(Optional.of(savedAddress.id()));
return orderRepo.insert(orderWithRefs, c);
}

/** Find all orders for a customer with their shipping addresses */
public List<CustomerOrderRow> findOrdersWithAddresses(CustomerId customerId, Connection c) {
return orderRepo.select()
.where(o -> o.customerId().eq(customerId))
.toList(c);
}
}

Benefits​

You still get huge benefits from using Typr:

  • All of this is type-safe
  • You get perfect auto-complete from your IDE
  • Strongly typed Id types and type flow ensure that you have to follow foreign keys correctly
  • It's testable! You can wire in mock repositories and test without a running database

Isn't this a service at this point?​

Maybe! You likely shouldn't use the generated Row types at the service level, and there should likely be a transaction boundary. You get to decide that, however.