Pivotal + VMware: Transforming how more of the world builds software

Modernization with Consumer Driven Contracts

Context

Modernizing an existing legacy application into multiple Microservices. These services may be related services representing multiple subdomains within a single bounded context, meaning that the development teams work closely together.

Problem

Problems consumer driven contracts address: - How can you add to an API without breaking downstream clients? - How can something be removed from a service without breaking downstream clients? - How can a service developer find out who is using their service? - How can a service developer release with short release cycles and continuous delivery?

Additional problems addressed by consumer driven contracts in a modernization effort: - How can a team know how much of the legacy functionality to add to a new service? - How can a team decide what order to add new functionality?

Forces

  • A team of developers may be building many related services at the same time as part of the modernization effort.
  • A team know the "domain language" of the bounded context, but doesn't know the individual properties of each aggregate and event payload.
  • The legacy application contains a large data model and existing service surface area, and the team doesn't want to port 100% of the legacy application to the new Microservices architecture. (Maybe not all of that legacy functionality is needed anymore. It's been around for a long time and no one really knows everything it does.)

Solution

In an event-driven architecture, many Microservices expose two kinds of APIs: a RESTful API over HTTP and a message-based API for publishing and subscribing to domain events. The messaging tier provides a mechanism for a constellation of Microservices to be loosely coupled as an emergent and reactive system. The RESTful API provides a means for integrating with these services in a synchronous fashion as well as to provide complex query capability for services that have received events from a service. By allowing consumers to provide contracts for both of these tiers, we can provide a prescribed language to our consumers that matches their needs.

Using Separate Test Base Classes for Consumers and Transport Types

Spring Cloud Contract allows you to control the base class that generated server tests use, and these allow us to customize the individual tests for our need with various mocks and configuration. The DNA project initializer sets up a contracts directory in the specification project and configures a single base class for all tests. Instead, we recommend setting up a directory structure such as the following to allow for tuning tests for individual consumers and separate HTTP/messaging concerns:

contracts/
 |- <consumer1>/
 |--- http/
 |----- shouldReturnResultWhenRequestIsMade.groovy
 |--- messaging/
 |----- shouldProduceSuccessMessageWhenRequestIsProcessed.groovy
 |- <consumer2>/
 |--- http/
 |--- messaging/
 ...

In your build.gradle file, you'll need to configure how this gets mapped to base classes. The following will generate tests with a base class made up of the last two segments of the package such as HttpBase or MessagingBase in the com.lmig.pli.rate.auto.autorateablequote.contract package:

contracts {
  packageWithBaseClasses: "com.rate.auto.autorateablequote.contract"
}

You can then provide those base class implementations as such:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { ServiceApplication.class })
@AutoConfigureMockMvc
@Import(WireMockConfiguration.class)
@ActiveProfiles("mock")
public abstract class Consumer1HttpBase {

    @Autowired
    private MockMvc mvc;

    @Before
    public void test() {
        RestAssuredMockMvc.mockMvc(this.mvc);
    }

}

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { ServiceApplication.class })
@AutoConfigureMockMvc
@AutoConfigureMessageVerifier
@Import(WireMockConfiguration.class)
@ActiveProfiles("mock")
public abstract class Consumer1MessagingBase {

    @Autowired
    private MockMvc mvc;

    @Before
    public void test() {
        RestAssuredMockMvc.mockMvc(this.mvc);
    }

    /**
     * This method is called from the triggeredBy() method in the contract DSL to
     * publish the message to be tested.
     */
    public void requestAutoRateApiIsCalled() {
        // given:
        MockMvcRequestSpecification request = RestAssuredMockMvc.given()
                .header("Content-Type", "application/json;charset=UTF-8")
                .body("{\"quote\":{\"jurisdiction\":\"NY\",\"policyEffectiveDate\":\"2017-05-25T15:04:05-04:00\",\"policyTransactionType\":\"01\",\"quoteEffectiveDate\":\"2017-05-25T15:04:05-04:00\",\"quoteId\":\"1\"}}");

        // when:
        ResponseOptions response = RestAssuredMockMvc.given().spec(request)
                .post("/rateableQuote");
    }
}

Contracts for RESTful APIs

def iso8601FormattedDatePattern = '(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2})\\:(\\d{2})\\:(\\d{2})(\\.\\d+)?[+-](\\d{2})\\:(\\d{2})'

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'POST'
        urlPath('/rateableQuote') {
            queryParameters {
            }
        }
        body([
                quote: [
                    jurisdiction: value(regex('[a-zA-Z]{2}')),
                    policyEffectiveDate: value(producer('2017-05-25T15:04:05.999-04:00'), consumer(regex(iso8601FormattedDatePattern))),
                    policyTransactionType: value(regex('[0-9]{2}')),
                    quoteEffectiveDate: value(producer('2017-05-25T15:04:05.999-04:00'), consumer(regex(iso8601FormattedDatePattern))),
                    quoteId: value(regex('[0-9]+'))
                ]
        ])
        headers {
            contentType("application/json;charset=UTF-8")
        }
    }

    response {
        status 200
        headers {
            header("Content-Type", "application/json;charset=UTF-8")
        }

        body([
                rate: '$5.00'
        ])
    }
}

Contracts for Messaging APIs

In order to create tests and stubs for services that communicate via messages over Spring Cloud Stream, you'll need to add a handful of dependencies to your build.gradle:

testCompile("org.springframework.cloud:spring-cloud-stream-test-support")

In order to test that your API controller emits a message as a side effect of an appropriate message call, you'll need to express that contract in the contract DSL as follows:

def iso8601FormattedDatePattern = '(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2})\\:(\\d{2})\\:(\\d{2})(\\.\\d+)?[+-](\\d{2})\\:(\\d{2})'

org.springframework.cloud.contract.spec.Contract.make {

    input {
        // the contract will be triggered by a method
        triggeredBy('requestAutoRateApiIsCalled()')
    }

    outputMessage {
        // in Spring Cloud Stream, this is the destination channel where the message is expected to be published
        sentTo 'rateable_quote'

        body([
                eventId: "1",
                type: "QUOTE_DATA_COLLECTED",
                entity: [
                        id: 1,
                        quoteId: value(regex('[0-9]+')),
                        policyEffectiveDate: value(regex(iso8601FormattedDatePattern)),
                        policyTransactionType: value(regex('[0-9]{2}')),
                        jurisdiction: value(regex('[a-zA-Z]{2}')),
                        quoteEffectiveDate: value(regex(iso8601FormattedDatePattern)),
                        createdAt: value(regex(iso8601FormattedDatePattern)),
                        lastModified: value(regex(iso8601FormattedDatePattern))
                ]
        ])
    }
}

You'll also need to implement the requestAutoRateApiIsCalled() method in your producer implementation's base class as described above so that generated tests will produce the message under test.

Resulting Context

An environment where a Microservice does not dictate the APIs it provides, but reacts to client needs by implementing them in a pull-based fashion.

Benefits

This inverts the classic architecture question "what data should my domain model contain?" The answer in this pattern is "nothing until a client says it needs something". It allows the development team to work backwards from the finish line to the starting point, creating a consumer that needs some data and then looking for the service that should provide that data in order to make a contract with it.

Drawbacks

This has proven difficult when the development team is working on all of the services at once and they control the whole set (as in a modernization effort). This environment makes it easy to do monolithic design where you try and get everything correct across the full set of services. Instead, it helps to strictly enforce that a team working on a story for a consuming service writes a contract expressing the events and APIs it needs to operate and having the implementing service discuss this with them as a true client.

Issues to Resolve

How does consumer driven contracts work when all services need all of the data in order to interact with other legacy services that assume full access to a large data model? In this case, it's much harder for a client to say "I need X, Y, and Z" and turns into many clients asking for the same large payload.

Related Patterns

Alternative Solutions

  • Up-front data modeling with a central modeling organization. This tightly couples services and inhibits rapid delivery.
  • Provider takes a top-down approach to defining their APIs as a prescribed language and expects the clients to work within the provided functionality.

Solutions to Problems Introduced by This Pattern

  • Strict processes around producer and consumer roles when working on stories integrating multiple services.
联系我们