Multi-tier application architecture with Microservices

By | October 30, 2016 | 255 Views

Microservice Architecture notes follow:

1. A stock market example. 4. Implementation.
2. Solution with Microservice. 5. Testing with Microservice.
3. Apply Microservices with Spring. 6. References.

1. A stock market example

  • In the stock market, settlement is all processes the buyer received his shares and the seller received his money after they bought or sold shares through his broker.
  • The period within which buyers receive their shares and sellers receive their money is called a settlement cycle.
    This period depends on the settlement cycle in different market. For instance, ASX Ltd (Australian Securities Exchange) used a T+2 settlement cycle for cash market trades in Australia. This means if a seller sells shares, his broker will receive money on the second working day.

Suppose that there are transactions clients have made in a settlement cycle presented by the Securities and Exchange Board:

TYPE  STOCK-VALUE  COMMISSION   TAX     CLIENT
BUY   100 MSFT@20  2000         5       CLSA (Credit Lyonnais Securities Asia)
SELL  100 MSFT@20  2100         5.5     CLSA (Credit Lyonnais Securities Asia)

Target is to build an application for illustrating trading transactions as on the Board.

2. Solution

2.1 Microservice architecture

microservice-architecture
Figure 1: Microservice architecture (source: martinfowler.com)

2.2 System architecture

System-architecture
Figure 2: Multi-services work together as a system to provide business features

  • Logic/module boundary contains service B which presented an id generator service for transactions made.
  • Logic/module boundary including service A will be transaction processes involved calling the api of service B.
  • Front-end will ask for a JSON representation of a transaction. REpresentational State Transfer (REST) used over HTTP protocol in the system.

3. Apply microservices with Spring.

3.1. Resource component.

According to Martinfowler,”Resources act as mappers between the application protocol exposed by the service and messages to objects representing the domain. Typically, they are thin, with responsibility for sanity checking the request and providing a protocol specific response according to the outcome of the business transaction.”. The figure below shows how to implement resource in a specific system.

microservice-resource-spring
Figure 3: Transaction resource Layer.

3.2. Domain components.

“Almost all of the service logic resides in a domain model representing the business domain. Of these objects, services coordinate across multiple domain activities, whilst repositories act on collections of domain entities and are often persistence backed.”

microservice-domain-spring
Figure 4: Transaction Domain Layer.

3.3. External components.

“If one service has another service as a collaborator, some logic is needed to communicate with the external service. A gateway encapsulates message passing with a remote service, marshalling requests and responses from and to domain objects. It will likely use a client that understands the underlying protocol to handle the request-response cycle.”

microservice-external-spring
Figure 5: Transaction External Layer.

3.4. Data components.

“Except in the most trivial cases or when a service acts as an aggregator across resources owned by other services, a micro-service will need to be able to persist objects from the domain between requests. Usually this is achieved using object relation mapping or more lightweight data mappers depending on the complexity of the persistence requirements.
Often, this logic is encapsulated in a set of dedicated objects utilised by repositories from the domain.”

microservice-persistance-spring
Figure 6: Transaction Persistance Layer.

4. Implementation.

As Figure 2, the two module boundaries named as settlement-transaction-idgenerator and settlement-transaction-management. The former provides a API that automatically generates a transaction ID combined by market code, comapany symbol in stock market and a sequence number. The latter handles transaction processes such as save, list, and get a particular transaction.

4.1. settlement-transaction-management module.

settlement-transaction-management-module
Figure 5: settlement-transaction-management structure.

Resource component code example.

src/main/java/com/mycompany/app/transaction/api/TransactionResource.java

package com.mycompany.app.transaction.api;

import com.mycompany.app.transaction.utils.Pagination;
import org.springframework.http.ResponseEntity;
import com.mycompany.app.transaction.api.dto.TransactionDTO;


public interface TransactionResource {

    /**
     * @api {POST} /api/transaction/add Add a new transaction.
     * @apiName addTransaction
     * @apiGroup transaction
     * @apiDescription Add a new transaction
     **@apiParamExample {json} Request-Example:
     * {
     *      "transactionCode": null,
     *      "companySymbol":"TSLA",
     *      "marketCode": "NY",
     *      "transactionType": "SELL",
     *      "stockValue": "@501",
     *      "commission": 100.5,
     *      "tax": 5.5,
     *      "client": "TESLA Technology",
     *      "createdOn":null
     * }
     * @apiSuccessExample Success-Response:
     *
     * HTTP/1.1 200 OK
     * {
     *   "transactionCode": "NYSE1100022",
     *   "companySymbol": "TSLA",
     *   "marketCode": "NY",
     *   "transactionType": "SELL",
     *   "stockValue": "@501",
     *   "commission": 100.5,
     *   "tax": 5.5,
     *   "client": "TESLA Technology",
     *   "createdOn": "2016-10-29 19:58:30"
     * }
     *
     *
     * @apiError TRANSACTION_ID_NOT_FOUND Could not find mapping for entity
     * @apiErrorExample Error-Response:
     * HTTP/1.1 404 Not Found
     * {
     *   "errorCode":"TRANSACTION_ID_NOT_FOUND",
     *   "errorMessage":"Transaction id is not generated"}
     * }
     *
     */
	ResponseEntity<Void> saveTransaction(TransactionDTO transactionDTO);

    Pagination<TransactionDTO> getTransactionList(Integer page, Integer pageSize);

    TransactionDTO findTransaction(String companySymbol,String transactionCode);
}

src/main/java/com/mycompany/app/transaction/api/TransactionResourceImpl.java

package com.mycompany.app.transaction.rest;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import com.mycompany.app.transaction.api.dto.TransactionDTO;
import com.mycompany.app.transaction.service.TransactionService;
import com.mycompany.app.transaction.utils.Pagination;
import com.mycompany.app.transaction.utils.RestException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.mycompany.app.transaction.api.TransactionResource;

@RestController
@RequestMapping("/api")
public class TransactionResourceImpl implements TransactionResource {
    private static final String APPLICATION_JSON_DEFAULT_CHARSET =
            APPLICATION_JSON_VALUE + ";charset=UTF-8";
    @Autowired
    private TransactionService transactionService;
    
    @Override
    @RequestMapping(value = "/transaction/add", method = RequestMethod.POST,
            produces = APPLICATION_JSON_DEFAULT_CHARSET)
    public ResponseEntity<Void> saveTransaction(
                                @RequestBody TransactionDTO transactionDTO) {
    	try{
            transactionService.saveTransaction(transactionDTO);
    		 return new ResponseEntity<>(HttpStatus.OK);
    	} catch (RestException e) {
            throw new RestException(e.getRestError(), e.getErrorParams());
        }
    }

    @Override
    @RequestMapping(value = "transaction/{companySymbol}/{transactionCode}",
            method = RequestMethod.GET,
            produces = APPLICATION_JSON_DEFAULT_CHARSET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public TransactionDTO findTransaction(@PathVariable String companySymbol, 
                                          @PathVariable String transactionCode) {
        return transactionService
               .findByTransactionCodeAndCompanySymbol(companySymbol, transactionCode);
    }

    @Override
    @RequestMapping(value = "/transaction/list", method = RequestMethod.GET,
            produces = APPLICATION_JSON_DEFAULT_CHARSET)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Pagination<TransactionDTO> getTransactionList(
                                          @RequestParam(value = "page") Integer page,
                                          @RequestParam(value = "pageSize") Integer pageSize) {
        return transactionService.getAllTransactionsByPaging(page, pageSize);
    }
}

Domain component code example.

src/main/java/com/mycompany/app/transaction/api/dto/TransactionDTO.java

package com.mycompany.app.transaction.api.dto;


import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.mycompany.app.transaction.domain.TransactionType;
import com.mycompany.app.transaction.utils.CustomDateSerializer;
import org.joda.time.DateTime;

@JsonIgnoreProperties(ignoreUnknown = true)
public class TransactionDTO extends DTO{

    private String transactionCode;
    private String companySymbol;
    private String marketCode;
    private TransactionType transactionType;
    private String stockValue;
    private Double commission;
    private Double tax;
    private String client;
    @JsonSerialize(using = CustomDateSerializer.class)
    private DateTime createdOn;
    //Setters and Getters

src/main/java/com/mycompany/app/transaction/service/TransactionService.java

package com.mycompany.app.transaction.service;

import com.mycompany.app.transaction.api.dto.TransactionDTO;
import com.mycompany.app.transaction.utils.Pagination;

public interface TransactionService {
    TransactionDTO saveTransaction(TransactionDTO transactionDTO);

    Pagination<TransactionDTO> getAllTransactionsByPaging(int page, int pageSize);

    TransactionDTO findByTransactionCodeAndCompanySymbol(String companySymbol, String transactionCode);
}

src/main/java/com/mycompany/app/transaction/service/TransactionServiceImpl.java

package com.mycompany.app.transaction.service;

import com.mycompany.app.transaction.api.dto.TransactionDTO;
import com.mycompany.app.transaction.domain.Transaction;
import com.mycompany.app.transaction.gateway.IDGeneratorGateway;
import com.mycompany.app.transaction.gateway.dto.IdCodeDTO;
import com.mycompany.app.transaction.repository.TransactionRepository;
import com.mycompany.app.transaction.service.mapper.TransactionMapper;
import com.mycompany.app.transaction.utils.Pagination;
import com.mycompany.app.transaction.utils.RestError;
import com.mycompany.app.transaction.utils.RestException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import static org.springframework.data.domain.Sort.Direction.DESC;

@Service
@Transactional
public class TransactionServiceImpl implements TransactionService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionServiceImpl.class);

    @Autowired
    private IDGeneratorGateway idGeneratorGateway;

    @Autowired
    private TransactionMapper transactionMapper;

    @Autowired
    private TransactionRepository transactionRepository;

    /**{@inheritDoc} **/
    @Override
    public TransactionDTO saveTransaction(TransactionDTO transactionDTO) {
        IdCodeDTO idCode = idGeneratorGateway.getIdCode(transactionDTO.getCompanySymbol());
        if(StringUtils.isEmpty(idCode.getCode())) {
            throw new RestException(new RestError("TRANSACTION_ID_NOT_FOUND",
                    "Transaction id is not generated", HttpStatus.NOT_FOUND));
        }
        LOGGER.debug("Save a transaction id {} with symbol {} in marketCode {}",
                idCode.getCode(), transactionDTO.getCompanySymbol(),transactionDTO.getMarketCode());
        transactionDTO.setTransactionCode(idCode.getCode());
        Transaction transaction = transactionMapper.toTransaction(transactionDTO);
        return transactionMapper.toTransactionDTO(transactionRepository.save(transaction));
    }

    /**{@inheritDoc} **/
    @Override
    public TransactionDTO findByTransactionCodeAndCompanySymbol(String companySymbol, String transactionCode) {
        Transaction transaction = transactionRepository
                .findByTransactionCodeAndCompanySymbol(companySymbol, transactionCode)
                .orElseThrow(() -> new RestException(new RestError("TRANSACTION_NOT_FOUND",
                        "Transaction is not found", HttpStatus.NOT_FOUND)));
        return transactionMapper.toTransactionDTO(transaction);
    }

    /**{@inheritDoc} **/
    @Override
    public Pagination<TransactionDTO> getAllTransactionsByPaging(int page, int pageSize) {
        final PageRequest pageRequest = new PageRequest(page, pageSize, new Sort(new Sort.Order(DESC, "createdOn")));
        Page<Transaction> transactionPage = transactionRepository.findAll(pageRequest);
        return convertToPagination(page, pageSize, transactionPage);
    }

    /** Convert current Page to customize Pagination
     */
    private Pagination<TransactionDTO> convertToPagination(int page, int pageSize, Page<Transaction> transactionPage) {
        Pagination<TransactionDTO> pagination  = new Pagination<>();
        pagination.setPage(page);
        Long total = transactionPage.getTotalElements();
        pagination.setTotalResult(total);
        if(total>0) {
            pagination.setTotalPages(total / pageSize + 1);
        }
        else{
            pagination.setTotalPages(0L);
        }
        pagination.getContent().addAll(transactionMapper.toTransactionDTOList(transactionPage.getContent()));
        return pagination;
    }

}

External component code example.

src/main/java/com/mycompany/app/transaction/gateway/IDGeneratorGateway.java

package com.mycompany.app.transaction.gateway;

import com.mycompany.app.transaction.gateway.dto.IdCodeDTO;

public interface IDGeneratorGateway {
    IdCodeDTO getIdCode(String symbol);
}

src/main/java/com/mycompany/app/transaction/gateway/IDGeneratorGatewayImpl.java

package com.mycompany.app.transaction.gateway;

import com.mycompany.app.transaction.gateway.dto.IdCodeDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;

@Service
public class IDGeneratorGatewayImpl implements IDGeneratorGateway {
	private final RestTemplate restTemplate;
    private final String tidGeneratorEndpoint;
    
    @Autowired
    public IDGeneratorGatewayImpl(@Value("${gateway.endpoints.idGenerator.url}") String tidGeneratorEndpoint,
                                  RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.tidGeneratorEndpoint = tidGeneratorEndpoint;
    }

    @Override
    public IdCodeDTO getIdCode(String symbol) {
        URI url = UriComponentsBuilder.fromHttpUrl(tidGeneratorEndpoint).queryParam("symbol", symbol).build().toUri();
        return restTemplate.getForObject(url, IdCodeDTO.class);
    }
}

Data component code example.

src/main/java/com/mycompany/app/transaction/repository/TransactionRepository.java

package com.mycompany.app.transaction.repository;

import com.mycompany.app.transaction.domain.Transaction;
import com.mycompany.app.transaction.domain.TransactionComposeKey;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface TransactionRepository extends JpaRepository<Transaction, TransactionComposeKey> {

    @Query(value="from Transaction t where t.transactionCode = :transactionCode " +
            "and t.companySymbol = :companySymbol")
    Optional<Transaction> findByTransactionCodeAndCompanySymbol(
            @Param(value = "companySymbol") String companySymbol,
            @Param(value = "transactionCode") String transactionCode);
}

src/main/java/com/mycompany/app/transaction/domain/Transaction.java

package com.mycompany.app.transaction.domain;

import com.mycompany.app.transaction.utils.DomainConstants;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;
import javax.persistence.*;

@Entity
@Table(name="transaction")
@IdClass(TransactionComposeKey.class)
public class Transaction implements DomainConstants {
    @Id
    private String transactionCode;

    @Id
    private String companySymbol;

    @Id
    private String marketCode;

    @Column(name = "transaction_type")
    @Enumerated(EnumType.STRING)
    private TransactionType transactionType;

    @Column(name = "stock_value")
    private String stockValue;

    @Column(name = "commission")
    private Double commission;

    @Column(name = "tax")
    private Double tax;

    @Column(name = "client")
    private String client;

    @Column(name = "created_on", updatable = false)
    @Type(type = COMMON_DATE_TYPE)
    private DateTime createdOn = new DateTime();

    // Override toString, equal and hashCode

src/main/java/com/mycompany/app/transaction/domain/Transaction.java

package com.mycompany.app.transaction.domain;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

/**
 * Defines the compound key used by the Transaction entity for persistence
 */
@Embeddable
public class TransactionComposeKey implements Serializable {
    @Column(name = "transaction_code")
    private String transactionCode;

    @Column(name = "company_symbol")
    private String companySymbol;

    @Column(name = "market_code")
    private String marketCode;
    // For JPA only
    protected TransactionComposeKey() {}

    public TransactionComposeKey(String transactionCode, String companySymbol, String marketCode) {
        this.transactionCode = transactionCode;
        this.companySymbol = companySymbol;
        this.marketCode = marketCode;
    }
    // Override Equal and hashCode

Utils code example.

src/main/java/com/mycompany/app/transaction/utils/DomainConstants.java

public interface DomainConstants {
    String COMMON_DATE_TYPE = "org.jadira.usertype.dateandtime.joda.PersistentDateTime";
}

src/main/java/com/mycompany/app/transaction/utils/CustomDateSerializer.java

package com.mycompany.app.transaction.utils;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;

/**
 * Created by Khiem on 10/29/2016.
 * JSON format joda datetime. Not recommend format at ModelMapper
 */
public class CustomDateSerializer extends JsonSerializer<DateTime> {
    private static DateTimeFormatter formatter =
            DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void serialize(DateTime value, JsonGenerator gen, SerializerProvider arg)
            throws IOException {
        gen.writeString(formatter.print(value));
    }
}

Resource configuration.

src/main/resources/application.yml

server:
    port: 8082
    address: 0.0.0.0
    
spring:
   datasource:
        dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
        test-on-borrow: true
        test-while-idle: true
        validation-query: SELECT 1
        validation-query-timeout: 30
        time-between-eviction-runs-millis: 300000
        url: jdbc:postgresql://localhost:5432/settlement_transaction_management
        databaseName: settlement_transaction_management
        serverName:
        username: postgres
        password: admin

   jpa:
           database-platform: org.hibernate.dialect.PostgreSQLDialect
           database: POSTGRESQL
           openInView: false
           show_sql: false
           generate-ddl: false
           hibernate:
               ddl-auto: none
               naming-strategy: org.hibernate.cfg.EJB3NamingStrategy
           properties:
               hibernate.cache.use_second_level_cache: false
               hibernate.cache.use_query_cache: false
               hibernate.generate_statistics: false
               hibernate.cache.region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory

gateway:
  configuration:
    readTimeout: 20000
    connectTimeout: 20000
  endpoints:
    idGenerator:
      url: http://localhost:8081/api/tidgenerator
liquibase:
    change-log: classpath:config/liquibase/db-changelog.xml

src/main/resources/config/liquibase/db-changelog.xml

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">

    <changeSet id="1" author="khiem.truong">
        <createTable tableName="transaction">
           <column name="transaction_code" type ="varchar(20)">
               <constraints nullable="false" />
           </column>
            <column name="company_symbol" type="varchar(10)">
                <constraints nullable="false"/>
            </column>
            <column name="market_code" type="varchar(10)">
                <constraints nullable="false"/>
            </column>
            <column name="transaction_type" type="varchar(255)">
                <constraints nullable="false"/>
            </column>
            <column name="stock_value" type="varchar(255)"/>
            <column name="commission" type="double"/>
            <column name="tax" type="double"/>
            <column name="client" type="varchar(255)"/>
            <column name="created_on" type="timestamp"/>
        </createTable>
        <addPrimaryKey columnNames="transaction_code,company_symbol,market_code,transaction_type" tableName="transaction"/>
    </changeSet>

</databaseChangeLog>

Make the application executable

src/main/java/com/mycompany/app/transaction/Application.java

package com.mycompany.app.transaction;

import ch.qos.logback.classic.LoggerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;

@Configuration
@ComponentScan(basePackages = {"com.mycompany.app.transaction"})
@EnableAutoConfiguration
@EnableJpaRepositories

public class Application {
    @Autowired
    private Environment env;

    private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);

    @PostConstruct
    public void initApplication() throws IOException {
    	 if (env.getActiveProfiles().length == 0) {
             LOGGER.warn("No Spring profile configured, running with default configuration");
         } else {
             LOGGER.info("Running with Spring profile(s) : {}", env.getActiveProfiles());
         }
    }

    public static void main(String[] args) {

        SpringApplication.run(Application.class, args);
       /* ApplicationContext ctx = SpringApplication.run(Application.class, args);

        String[] beanNames = ctx.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }*/
    }
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @PreDestroy
    public void shutdownLogback(){
        LOGGER.info("Shutting down Logback");
        LoggerContext lCtx = (LoggerContext) LoggerFactory.getILoggerFactory();
        lCtx.stop();
    }

}

4.2. settlement-transaction-idgenerator module.

Resource exposes generated transaction ID
src/main/java/com/mycompany/app/idgenerator/rest/TransactionIdGeneratorResourceImpl.java


import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.mycompany.app.idgenerator.api.IdGeneratorResource;
import com.mycompany.app.idgenerator.api.dto.TransactionIdDTO;
import com.mycompany.app.idgenerator.service.TransactionIdService;

@RestController
@RequestMapping("/api")
public class TransactionIdGeneratorResourceImpl implements IdGeneratorResource {

    @Autowired
    private TransactionIdService idCodeService;

    @RequestMapping(value = "tidgenerator",
            method = RequestMethod.GET,
            produces = APPLICATION_JSON_VALUE)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public TransactionIdDTO generateIdentificationCode(
               @RequestParam(value = "symbol") String symbol) {
        return idCodeService.generateIdentificationCode(symbol);
    }

}

Run the applications of the two modules and using any Rest Client to test APIs:

GET http://localhost:8081/api/tidgenerator?symbol=TSLA
Success-Response:
HTTP/1.1 200 OK
{
  "code": "NYSE1100019"
}
POST http://localhost:8082/api/transaction/add
{
	"transactionCode": null,
	"companySymbol":"TSLA",
	"marketCode": "NY",
	"transactionType": "SELL",
	"stockValue": "@501",
	"commission": 100.5,
	"tax": 5.5,
	"client": "TESLA Technology",
	"createdOn":null
}
Success-Response:
HTTP/1.1 200 OK
{
 {
	"transactionCode": "NYSE1100019",
	"companySymbol":"TSLA",
	"marketCode": "NY",
	"transactionType": "SELL",
	"stockValue": "@501",
	"commission": 100.5,
	"tax": 5.5,
	"client": "TESLA Technology",
	"createdOn":"2016-10-29 19:58:30"
}
}
GET http://localhost:8082/api/transaction/TSLA/NYSE1100018
Success-Response:
HTTP/1.1 200 OK
{
  "transactionCode": "NYSE1100018",
  "companySymbol": "TSLA",
  "marketCode": "NY",
  "transactionType": "SELL",
  "stockValue": "@501",
  "commission": 100.5,
  "tax": 5.5,
  "client": "TESLA Technology",
  "createdOn": "2016-10-29 17:11:32"
}
GET http://localhost:8082/api/transaction/list?page=0&pageSize=10
Success-Response:
HTTP/1.1 200 OK
{
  "content": [
    {
      "transactionCode": "NYSE1100022",
      "companySymbol": "TSLA",
      "marketCode": "NY",
      "transactionType": "SELL",
      "stockValue": "@501",
      "commission": 100.5,
      "tax": 5.5,
      "client": "TESLA Technology",
      "createdOn": "2016-10-29 19:58:30"
    },
    {
      "transactionCode": "NYSE1100021",
      "companySymbol": "TSLA",
      "marketCode": "NY",
      "transactionType": "SELL",
      "stockValue": "@501",
      "commission": 100.5,
      "tax": 5.5,
      "client": "TESLA Technology",
      "createdOn": "2016-10-29 19:58:28"
    }
  ],
  "page": 0,
  "totalResult": 2,
  "totalPages": 1
}

Seem it works fine but not enough for a stable system. we need to include testing.

5. Testing with Microservice.

Testing makes sure that all APIs/functions work properly. When someone tries to modify some codes, those changes potentially affect on whole system. Therefore, sufficient testing e.g unit, integration tests..together prevents us from breaking the behavior of the system.

5.1 Testing Services.

To mock a API, Using MockRestServiceServer calls a request for the generate transaction ID.
src/test/java/com/mycompany/app/TransactionServiceITTest.java

package com.mycompany.app;

import com.mycompany.app.transaction.Application;
import com.mycompany.app.transaction.api.dto.TransactionDTO;
import com.mycompany.app.transaction.domain.Transaction;
import com.mycompany.app.transaction.domain.TransactionComposeKey;
import com.mycompany.app.transaction.domain.TransactionType;
import com.mycompany.app.transaction.repository.TransactionRepository;
import com.mycompany.app.transaction.service.TransactionService;
import com.mycompany.app.transaction.utils.Pagination;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import java.util.stream.IntStream;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

/**
 * Created by Khiem on 10/29/2016.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebAppConfiguration
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@ActiveProfiles("devmock")
public class TransactionServiceITTest {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private TransactionService transactionService;

    @Autowired
    private TransactionRepository transactionRepository;

    private MockRestServiceServer mockServer;

    @Before
    public void setup() {
        mockServer = MockRestServiceServer.createServer(restTemplate);
        IntStream.range(1, 3).forEach(i -> {
            mockServer.expect(requestTo(
                            "http://localhost:8081/api/tidgenerator?symbol=TSLA"))
                    .andExpect(method(HttpMethod.GET)).andRespond(
                    withSuccess().contentType(MediaType.APPLICATION_JSON).body(
                            "{\"code\": \"NYSE110001" + i + "\"}"));
        });
    }

    @Test
    public void addTransactionSuccessTest() {
        try {
            TransactionDTO transactionDTO = new TransactionDTO();
            transactionDTO.setCompanySymbol("TSLA");
            transactionDTO.setMarketCode("NY");
            transactionDTO.setTransactionType(TransactionType.SELL);
            transactionDTO.setStockValue("@501");
            transactionDTO.setCommission(100.5);
            transactionDTO.setTax(5.5);
            transactionDTO.setClient("TESLA Technology");
            transactionService.saveTransaction(transactionDTO);
            TransactionComposeKey key = new TransactionComposeKey("NYSE1100011","TSLA","NY");
            Transaction transaction = transactionRepository.findOne(key);
            Assert.assertNotNull(transaction);
            Assert.assertEquals("NYSE1100011", transaction.getTransactionCode());
            Assert.assertEquals("TSLA", transaction.getCompanySymbol());
            Assert.assertEquals("NY", transaction.getMarketCode());
            Assert.assertNotNull(transaction.getCreatedOn());

        } finally {
            transactionRepository.deleteAll();
        }
    }
    @Test
    public void getTransactionSuccessTest() {
        try {
            TransactionDTO transactionDTO = new TransactionDTO();
            transactionDTO.setCompanySymbol("TSLA");
            transactionDTO.setMarketCode("NY");
            transactionDTO.setTransactionType(TransactionType.SELL);
            transactionDTO.setStockValue("@501");
            transactionDTO.setCommission(100.5);
            transactionDTO.setTax(5.5);
            transactionDTO.setClient("TESLA Technology");
            transactionService.saveTransaction(transactionDTO);
            TransactionDTO transactionDTO1 = transactionService.
                    findByTransactionCodeAndCompanySymbol("TSLA","NYSE1100011");
            Assert.assertEquals("NYSE1100011", transactionDTO1.getTransactionCode());
            Assert.assertEquals("TSLA", transactionDTO1.getCompanySymbol());
            Assert.assertEquals("NY", transactionDTO1.getMarketCode());
        } finally {
            transactionRepository.deleteAll();
        }
    }
    @Test
    public void getTransactionPaginationSuccessTest() {
        try {
            // Create a transaction SELL
            TransactionDTO transactionDTO = new TransactionDTO();
            transactionDTO.setCompanySymbol("TSLA");
            transactionDTO.setMarketCode("NY");
            transactionDTO.setTransactionType(TransactionType.SELL);
            transactionDTO.setStockValue("@501");
            transactionDTO.setCommission(100.5);
            transactionDTO.setTax(5.5);
            transactionDTO.setClient("TESLA Technology");
            transactionService.saveTransaction(transactionDTO);

            // Create a transaction BUY
            transactionDTO = new TransactionDTO();
            transactionDTO.setCompanySymbol("TSLA");
            transactionDTO.setMarketCode("NY");
            transactionDTO.setTransactionType(TransactionType.BUY);
            transactionDTO.setStockValue("@502");
            transactionDTO.setCommission(100.5);
            transactionDTO.setTax(5.4);
            transactionDTO.setClient("TESLA Technology");
            transactionService.saveTransaction(transactionDTO);

            Pagination<TransactionDTO> transactionPagination =  transactionService.getAllTransactionsByPaging(0,10);
            Assert.assertNotNull(transactionPagination.getContent());
            Assert.assertEquals("Should be page #0", 0L, transactionPagination.getPage());
            Assert.assertEquals("Should be one page", 1L, transactionPagination.getTotalPages());
            Assert.assertEquals("Found two transactions", 2, transactionPagination.getTotalResult());

        } finally {
            transactionRepository.deleteAll();
        }
    }
}

5.2 Testing API.

Using MockMvc and MockRestServiceServer to mock the transaction APIs. Other ways, cucumber-junit, cucumber-spring,..are powerful integration testing.
src/test/java/com/mycompany/app/TransactionAPITest.java

package com.mycompany.app;

import com.mycompany.app.transaction.Application;
import com.mycompany.app.transaction.api.dto.TransactionDTO;
import com.mycompany.app.transaction.domain.TransactionType;
import com.mycompany.app.transaction.service.TransactionService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.stream.IntStream;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

/**
 * Created by Khiem on 10/29/2016.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class TransactionAPITest {

    private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));

    private MockMvc mockMvc;

    private MockRestServiceServer mockServer;

    private HttpMessageConverter mappingJackson2HttpMessageConverter;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private TransactionService transactionService;

    @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {

        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
                hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();

        Assert.assertNotNull("the JSON message converter must not be null",
                this.mappingJackson2HttpMessageConverter);
    }
    @Before
    public void setup() throws Exception {

        this.mockMvc = webAppContextSetup(webApplicationContext).build();
        mockServer = MockRestServiceServer.createServer(restTemplate);

        IntStream.range(1, 3).forEach(i -> {
            mockServer.expect(requestTo(
                    "http://localhost:8081/api/tidgenerator?symbol=TSLA"))
                    .andExpect(method(HttpMethod.GET)).andRespond(
                    withSuccess().contentType(MediaType.APPLICATION_JSON).body(
                            "{\"code\": \"NYSE110001" + i + "\"}"));
        });

        initiateData();
    }



    @Test
    public void createTransaction() throws Exception {
        // Client SELL @501 TSLA
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setCompanySymbol("TSLA");
        transactionDTO.setMarketCode("NY");
        transactionDTO.setTransactionType(TransactionType.SELL);
        transactionDTO.setStockValue("@501");
        transactionDTO.setCommission(100.5);
        transactionDTO.setTax(5.5);
        transactionDTO.setClient("TESLA Technology");

        mockMvc.perform(post("http://localhost:8082/api/transaction/add")
                .content(this.json(transactionDTO))
                .contentType(contentType))
                .andExpect(status().isOk());
    }

    @Test
    public void readSingleTransaction() throws Exception {
        mockMvc.perform(get("http://localhost:8082/api/transaction/TSLA/NYSE1100012"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$.transactionCode", is("NYSE1100012")))
                .andExpect(jsonPath("$.companySymbol", is("TSLA")))
                .andExpect(jsonPath("$.marketCode", is("NY")))
                .andExpect(jsonPath("$.transactionType", is("SELL")))
                .andExpect(jsonPath("$.stockValue", is("@501")))
                .andExpect(jsonPath("$.commission", is(100.5)))
                .andExpect(jsonPath("$.tax", is(5.5)))
                .andExpect(jsonPath("$.client", is("TESLA Technology")));

    }

    @Test
    public void getTransactionlist() throws Exception {
        // Execute API to get list of transactions.
        String url = "http://localhost:8082/api/transaction/list?page=0&pageSize=10";
        mockMvc.perform(get(url))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$.content", hasSize(2)))
                .andExpect(jsonPath("$.content.[0].transactionCode", is("NYSE1100012")))
                .andExpect(jsonPath("$.content.[0].companySymbol", is("TSLA")))
                .andExpect(jsonPath("$.content.[0].marketCode", is("NY")))
                .andExpect(jsonPath("$.content.[0].transactionType", is("SELL")))
                .andExpect(jsonPath("$.content.[0].commission", is(100.5)))
                .andExpect(jsonPath("$.content.[1].transactionCode", is("NYSE1100011")))
                .andExpect(jsonPath("$.content.[1].companySymbol", is("TSLA")))
                .andExpect(jsonPath("$.content.[1].marketCode", is("NY")))
                .andExpect(jsonPath("$.content.[1].transactionType", is("BUY")))
                .andExpect(jsonPath("$.content.[1].commission", is(100.4)));
    }

    protected String json(Object o) throws IOException {
        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
        this.mappingJackson2HttpMessageConverter.write(
                o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
        return mockHttpOutputMessage.getBodyAsString();
    }

    private void initiateData() throws Exception{
        // Client BUY @501 TSLA
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setCompanySymbol("TSLA");
        transactionDTO.setMarketCode("NY");
        transactionDTO.setTransactionType(TransactionType.BUY);
        transactionDTO.setStockValue("@501");
        transactionDTO.setCommission(100.4);
        transactionDTO.setTax(5.3);
        transactionDTO.setClient("TESLA Technology");
        transactionService.saveTransaction(transactionDTO);
    }

}

References:

  1. Rodriguez, Alex. “RESTful Web services: The basics“. IBM developerWorks. IBM. Retrieved 30 Oct, 2016 (POST vs PUT – PUT is idempotent, so if you PUT an object twice, it has no effect. This is a nice property, so I would use PUT when possible.)
  2. Toby Clemson, “Testing Strategies in a Microservice Architecture”, martinfowler. Retrieved Oct 30, 2016 at http://www.martinfowler.com/articles/microservice-testing/
  3. Jim W., Savas P. and Lan Robinson, REST in Practice, O’Reilly Media,2010.
  4. Sam Newman, “Rest”, in Buiding Microservice, O’Reilly Media, 2015.
  5. Addison Wesley – Enterprise Integration Patterns – Designing, Building And Deploying Messaging Solutions – With Notes
  6. JavaTM Persistence 2.1 Final Release for Evaluation, Oracle. Retrieved Oct 30, 2016 at
    http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html
  7. Building REST services with Spring, Pivotal Software, Inc. Retrieved Oct 30, 2016 at
    https://spring.io/guides/tutorials/bookmarks/
  8. Transition to T+2 Settlement for Cash Equities, ASX Limited. Retrieved Oct 30, 2016 at
    http://www.asx.com.au/services/t2.htm

One thought on “Multi-tier application architecture with Microservices

  1. khiem Post author

    Question: Why does entity classes (TransactionComposeKey) require at least a public or protect no-argument constructor (the class may have other constructors)?
    Answer: Hibernate can produce a proxy (looks like a placeholder) which is an instance of a runtime -generated subclass of the class (TransactionComposeKey.class), carrying the identifier value of the entity instance it represents.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *