Remote Proxy Pattern

Assuming the actual instance to be a remote service instance, we can use the design pattern to implement a client server model allowing us to segregate the complex business logic as a separate layer.

The proxy design pattern helps us in scenarios where we want to substitute actual instance with a placeholder. Assuming the actual instance to be a remote service instance, we can use the design pattern to implement a client-server model allowing us to segregate the complex business logic as a separate layer.

Another advantage of this implementation is the minimal footprint of code (mostly boilerplate), required to manage request-response, in case there are multiple services with more than 1 method.

Components

  1. Service Contract - used by both the service provider and the consumer. The same is commonly provided as a jar containing the service definitions as java interface(s).
  2. Service Implementation - remote service providing the actual service implementation implementing the contract.
  3. Service Consumer - the client that triggers the requests on the service provider by using remote proxies created for the shared service contract.

Implementation

The following section provides an overview of the various modules and their implementation:

Defining the service contract: Consider a demo RoomBookingService that provides the following features:

  1. Check the room availability for a given time slot.
  2. Book the room for the given slot.
  3. Cancel booking for a given booking id.
public interface RoomBookingService {
    Boolean isAvailable(LocalDate from, LocalDate to, int count);
    Long book(LocalDate from, LocalDate to, int count) throws BookingException;
    Boolean cancel(long bookingId) throws BookingException;
}

The package containing the RoomBookingService is added as a dependency in both service-impl and service-consumer as a dependency.

<dependency>
    <groupId>com.jvmaware</groupId>
    <artifactId>service-contract</artifactId>
    <version>${service.contract.version}</version>
</dependency>

Implementing the service contract: The next step is to implement the contract as a SpringBoot web service. The class BookingEndpoint has three endpoints corresponding to the methods in RoomBookingService.

@RestController
@RequestMapping(value = "/booking")
public class BookingEndpoint {
    
    private final RoomBookingService roomBookingService;

    public BookingEndpoint(RoomBookingService roomBookingService) {
        this.roomBookingService = roomBookingService;
    }

    @PostMapping(value = "/new/{count}")
    public long book(@PathVariable int count,
                     @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate from,
                     @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate to) {
        logger.info("booking request received from: [{}] to: [{}] for [{}] rooms", from, to, count);
        return roomBookingService.book(from, to, count);
    }

    @GetMapping(value = "/check/{count}")
    public boolean checkAvailability(@PathVariable int count,
                                     @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate from,
                                     @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate to) {
        logger.info("checking room [{}] availability from [{}] to [{}]", count, from, to);
        return roomBookingService.isAvailable(from, to, count);
    }

    @PostMapping(value = "/cancel/{bookingId}")
    public boolean book(@PathVariable int bookingId) {
        logger.info("cancel request received for id: [{}]", bookingId);
        return roomBookingService.cancel(bookingId);
    }
}

This in turn delegates the requests to RoomBookingServiceImpl class which provides the concrete implementation for these methods.

public class RoomBookingServiceImpl implements RoomBookingService {
    @Override
    public Boolean isAvailable(LocalDate from, LocalDate to, int count) {
        // some implementation to check the room availability here
        // sending a default value for DEMO
        logger.info("room availability checked completed");
        return true;
    }
    
    @Override
    public Long book(LocalDate from, LocalDate to, int count) throws BookingException {
        // some implementation to book the rooms
        // sending a default booking id for DEMO
        logger.info("booking completed successfully");
        return randomNumberGenerator.nextLong();
    }
    
    @Override
    public Boolean cancel(long bookingId) throws BookingException {
        // some implementation to cancel the room booking
        // sending a default value for DEMO
        logger.info("booking cancelled successfully");
        return true;
    }
}

Implementing the service consumer: When implementing the consumer, we create a remote proxy for the RoomBookingService via ServiceProvider:

public class ServiceProvider {

    private static final ConfigProvider configProvider = new ConfigProvider();
    public static RoomBookingService roomBookingService() {
        return RoomBookingServiceProvider.INSTANCE;
    }

    private static class RoomBookingServiceProvider {
        private static final RoomBookingService INSTANCE =
                (RoomBookingService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                        new Class[]{RoomBookingService.class},
                        new ServiceInvocationHandler(configProvider));
    }
}

The ServiceInvocationHandler is responsible for managing connections to the remote service and result transformation:

public class ServiceInvocationHandler implements InvocationHandler {

    private final ConfigProvider configProvider;
    private final HttpClient client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2)
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();

    public ServiceInvocationHandler(ConfigProvider configProvider) {
        this.configProvider = configProvider;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        var config = configProvider.getConfig();
        var baseUrl = config.getBaseUrl();
        var httpMethodAndUrl = config.getMappings().get(method.getName());

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(MessageFormat.format(baseUrl + httpMethodAndUrl.getEndPoint(), args)))
                .timeout(Duration.ofMinutes(1))
                .header("Content-Type", "application/json")
                .method(httpMethodAndUrl.getMethod(), HttpRequest.BodyPublishers.noBody())
                .build();

        Supplier<?> body = client.send(request, new CustomBodyHandler<>(method.getReturnType())).body();
        return body.get();
    }
}

The actual endpoint details like resource identifier, method etc are resolved using ConfigProvider that load these details from a config file.

Finally, the requests are triggered from the client code which is totally abstracted from the underlying remote connections:

private void triggerInvocations() {
    var roomBookingService = ServiceProvider.roomBookingService();
    var now = LocalDate.now();
    var tomorrow = now.plus(1, ChronoUnit.DAYS);
    int requestedRoomCount = 2;

    // rooms are available; book rooms
    if(roomBookingService.isAvailable(now, tomorrow, requestedRoomCount)){
        logger.info("[{}] rooms are available for: [{}] and [{}]", requestedRoomCount, now, tomorrow);
        roomBookingService.book(now, tomorrow, requestedRoomCount);
    }
}

All the method calls on the proxy roomBookingService are captured by the ServiceInvocationHandler#invoke which then use the ConfigProvider to extract the actual endpoint details and the request method. A HttpClient instance is used to create connections and provide the results back to the client.

From an end-user perspective, the methods are called on a service instance as those are available locally. This allows us to segregate the actual client code from the underlying request-response mechanism.

You can check this repository for more details and code.

Be notified of new posts. Subscribe to the RSS feed.