Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Microservices or not? A dependency management decision

Jorge Franco
October 20, 2023
44

Microservices or not? A dependency management decision

In my company Magnolia, in the Labs team, we have been exploring different ways to move our platform to the cloud. There are architecture decisions to look for, but the use cases and the domain are the same. We think starting with microservices is not a good approach because introducing this complexity will make us develop much slower. With some of the best features of Java, such as interfaces and implementations, modules, testing, dependency management and Jakarta standards, we have created an application that can be deployed as a monolithic or as different microservices. We can deploy our application in different ways and test it at different levels. We will explore an example with different modules and connections between them. I will show the different ways you can test your modules. The code is in Java and uses Quarkus to deploy, but any JVM environment can be used. Finally, we will see all the benefits of using this modular approach to develop, test and deploy your applications.

Jorge Franco

October 20, 2023
Tweet

Transcript

  1. About me: - Working at Magnolia, in labs team, as

    developer - Living in Sevilla, Spain - JVM languages from Java 1.2 - Testing & Simplicity, complexity is the evil - Website: https://www.yila.dev - Github: https://github.com/chiquitinxx - E-mail: [email protected] - Twitter X: @jfrancoleza
 Jorge Franco
  2. What do we want? - Platform ready for the cloud

    - Offer similar capabilities to our clients - Remove coupling - Modern web interface - Easy to develop, test and operate - Multi-tenancy - A lot more things… So, let’s do a PoC for Magnolia SaaS Jorge Franco
  3. This talk is about: - Java standards - How did

    we achieve modularity? - Does it fit in my projects? - Testing - Tips - Benefits - Drawbacks Jorge Franco Not about: - Specific libraries / frameworks / tools - DDD, hexagonal, … - Queues, events, asynchronous,… but is the same
  4. Domain Interfaces package info.magnolia.demo.car.estimator; import java.math.BigDecimal; import dev.yila.functional.Result; public interface

    CarValueEstimator { Result<BigDecimal> calculate(EstimateRequest request); } public record EstimateRequest(Car car, BigDecimal priceNew) {} public record Car(String brand, int manufacturedYear, long km) {} Jorge Franco
  5. - External interfaces are not domain interfaces - It is

    a good idea put a domain interface when you use external libraries - Coupling 0 - Define your module, it is the way other modules use it - It is a good exercise define tests with the interfaces, not only the implementation Jorge Franco
  6. “API” module - The public entry point - No external

    dependencies - Domain interfaces and domain entities (pojo’s) Jorge Franco
  7. Domain interface testing (DIT)(aka TCKs) public abstract class EstimatorTest {

    @Test public void calculation() { var estimator = newEstimator(); assertEquals(17661, …); assertEquals(13746, …); } @Test public void moreKmsHasLessValue() {} @Test public void notSoGoodBrandHasLessValue() {} @Test public void olderCarHasLessValue() {} protected abstract CarValueEstimator newEstimator(); } Jorge Franco
  8. Testing and implementation public class EstimatorImpl implements CarValueEstimator { @Override

    public Result<BigDecimal> calculate(EstimateRequest request) {…} } public class EstimatorImplTest extends EstimatorTest { @Override protected CarValueEstimator newEstimator() { return new EstimatorImpl(); } } Jorge Franco
  9. - Add specific tests for your implementation - Mock external

    libraries or modules - Add dependency injection annotations if you prefer it (as @ApplicationScoped) - Use standard libraries as jakarta-ee and microprofile - Avoid specific framework tooling Jorge Franco
  10. Ready to be used - Changes in car-app to use

    the estimator module - Inject the interface where do we want to use - Add estimator-impl dependency in runtime environments - When you use estimator module in other modules, only need to add estimator-api dependency <dependency> <groupId>${project.groupId}</groupId> <artifactId>estimator-impl</artifactId> </dependency> @ApplicationScoped public class EstimatorImpl implements CarValueEstimator {…} Jorge Franco
  11. @Path("/car") public class CarResource { private static final BigDecimal PRICE_NEW

    = new BigDecimal("29000"); private final CarRepository carRepository; private final CarValueEstimator carValueEstimator; @Inject public CarResource(CarRepository carRepository, CarValueEstimator carValueEstimator) { this.carRepository = carRepository; this.carValueEstimator = carValueEstimator; } @GET @Path("/{carId}") @Produces(MediaType.APPLICATION_JSON) public Response carData(@PathParam("carId") String carId) { var car = carRepository.byId(carId); return carValueEstimator.calculate(new EstimateRequest(new Car(…), PRICE_NEW)) .map(nowValue -> new CarInfo(…, PRICE_NEW, nowValue)) .map(carInfo -> Response.ok(carInfo).build()) .orElse(result -> Response.status(400, result.getFailuresToString()).build()); } } Jorge Franco
  12. Create the endpoints import jakarta.inject.Inject; import jakarta.ws.rs.*; @Path("/estimator") public class

    EstimatorEndpoint { @Inject public EstimatorEndpoint(CarValueEstimator estimator) { this.estimator = estimator; } @POST @Path("/calculate") @Produces(APPLICATION_JSON) @Consumes(APPLICATION_JSON) public Response calculate(EstimateRequest request) { return this.estimator.calculate(request) .map(value -> Map.of("value", value)) .map(map -> Response.ok(map).build()) .orElse(result -> Response.status(400) .entity(Map.of("error", result.getFailuresToString())).build()); } } Jorge Franco
  13. - Only api dependency, not impl - Use Jakarta standards

    - Consider use JerseyTest or similar - Easy to automate - You can add openApi spec Jorge Franco
  14. Provide http client import org.eclipse.microprofile.rest.client.inject.RestClient; @ApplicationScoped public class EstimatorHttpClient implements

    CarValueEstimator { private final EstimatorRestClient estimatorRestClient; @Inject public EstimatorHttpClient(@RestClient EstimatorRestClient estimatorRestClient) { this.estimatorRestClient = estimatorRestClient; } @Override public Result<BigDecimal> calculate(EstimateRequest request) { var result = estimatorRestClient.calculate(request); … } } Jorge Franco
  15. - I use microprofile rest client - Use http client

    you prefer - Implements the API domain interface - You can create different clients, like gRPC, or events Jorge Franco
  16. Package estimator microservice - Empty sources - Quarkus dependencies +

    endpoints + impl - You can use your favourite framework - In this case we are packaging one module Jorge Franco
  17. Testing the service @QuarkusTest public class EstimatorEndpointTest { @Test public

    void calculate() { var request = new EstimateRequest(new Car("Volkswagen", 2016, 100000), new BigDecimal("23500")); with().body(request).header("Content-Type", "application/json").when() .post("/estimator/calculate") .then() .statusCode(200).log().all() .body("value", is(9047.500f)); } } Jorge Franco
  18. Testing the client @QuarkusIntegrationTest public class HttpClientIT extends EstimatorTest {

    @Override protected CarValueEstimator newEstimator() { var restClient = RestClientBuilder.newBuilder() .baseUri(URI.create("http://localhost:8081")) .build(EstimatorRestClient.class); return new EstimatorHttpClient(restClient); } } Jorge Franco
  19. Provide developing and testing facilities - Provide ways of test

    or use your modules from applications. - Quarkus has nice support for devservices and test containers facilities. - Provide infrastructure requirements of your modules as databases. Jorge Franco
  20. Testing & debugging all together - Now you can decide

    how you test or debug your modules and applications. - Just add the dependency that you want to your projects. - Example: a project with all implementation dependencies and no endpoints. - So now you can debug all the code of all your modules from the IDE. - Docker, containers, k8s, remote debugging,… only when you want, easy to setup and use. - Test in real environments as easy as create a new DIT implementation. Jorge Franco
  21. Does it fit for all use cases? No. Think 30

    times before introduce something new. This is good for new applications, new modules or new features. Modules & DIT’s fits very well in multi-cloud environments. Very good if you are going to write all your code in JVM languages. Jorge Franco
  22. Tips - Avoid complex dependency injection setups, just handle it

    in target applications. - Use OpenAPI specs in you endpoints modules. - Take care of modules, maybe you create too much. - Create your conventions for naming and follow in all modules. - When you have your version one ready, think about create DTO’s in your endpoints. - Only your tests in production / staging environments will give you confidence. - Automate generation of http-client and endpoint modules. - Think about changes in your domain interfaces, and how to manage them. Jorge Franco
  23. Benefits - Easier to develop, debug and test - Almost

    0 coupling - Good separation of the logic of your modules and applications - Easy to change implementations and frameworks - Multiple ways of deploy your apps, including lambdas Jorge Franco
  24. Drawbacks - More code than just create the microservices -

    Different objects with similar data Jorge Franco
  25. Finishing - Domain interfaces ftw! - Nothing can beat the

    experience of debug your hole application easily. - Reduce coupling is your main target. - Move the cloud complexity away from your code. - Make the life of your teams easier, provide tooling to use your module. - Don’t trust me, try it. - DIT’s are amazing, use them if you have more than 1 implementation. - Modules first, microservices maybe later. Jorge Franco