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

Case of Ad Delivery System is Implemented by Scala and DDD

Case of Ad Delivery System is Implemented by Scala and DDD

ScalaMatsuri 2019

AGAWA Koji

June 28, 2019
Tweet

More Decks by AGAWA Koji

Other Decks in Technology

Transcript

  1. Case of Ad Delivery System
    is Implemented by Scala
    and DDD
    阿川 耕司 (AGAWA Koji)
    AdTech Studio / CyberAgent, Inc.

    View Slide

  2. AGAWA Koji
    •Software engineer at CyberAgent, Inc.
    Work on Scala for 4 years
    Work on DDD for a year
    阿川 耕司
    atty303
    @atty303

    View Slide

  3. 1.What’s Ad Delivery System?
    2.Our Domain Driven Design
    3.Architecture
    4.Domain Layer
    5.Summary

    View Slide

  4. What’s Ad Delivery
    System?
    広告配信システムとは?

    View Slide

  5. Ecosystem of online advertisement
    ネット広告はこのような構造になっています

    View Slide

  6. Our system is DSP
    今回お話するシステムはこの中のDSPになります

    View Slide

  7. DSP is system for RTB
    DSPはRTB(Real Time Bidding)のシステムです
    https://blog.adcash.com/what-is-rtb-and-why-it-is-important/

    View Slide

  8. Programmatic Ad Buying as a Service
    GlazeというDSPを作っています
    Our DSP is ...

    View Slide

  9. Why Glaze?
    •Delivering ads with RTB has become generalized
    •Things other than RTB, such as creativity, become important
    •Glaze provide basic functions such as bidding, delivery and measurement required
    by RTB as SaaS
    •Glaze goal is to be able to focus on the intrinsic strengths of the customer's
    products
    なぜGlazeを作っているのかは本題ではないので簡単に説明し
    ます

    View Slide

  10. Why have we chosen DDD?
    • Probably 8th times (!) to make DSP with Adtech Studio ...
    • Out motivation is to create something that can withstand long-term
    maintenance
    • We have decided to introduce DDD as a proper design method
    長期的に持続できるシステムを作るためにDDDを選択しました

    View Slide

  11. Our Domain Driven Design
    DDDによる設計

    View Slide

  12. History of development
    • Focused on domain modeling early in development
    • Coded only the domain layer
    • Working with a small team
    • We discussed the domain model deeply
    Domain modeling
    (2~3 persons)
    Mob programming
    (4~5 persons)
    開発の初期は小さなチームでドメインモデリングに集中しまし

    2018/05 2018/08 2018/10
    Development continues…
    (4~8 persons)

    View Slide

  13. History of development
    • After the first domain modeling was done, we proceeded development with
    mob programming
    • Coding until one function works completely
    • Working with a middle team
    • We proceeded with agreement on architecture and coding conventions
    Domain modeling
    (2~3 persons)
    Mob programming
    (4~5 persons)
    ドメインモデリングが終わった後はモブプログラミングでチーム
    内の意識を合わせながら開発を進めました
    2018/05 2018/08 2018/10
    Development continues…
    (4~8 persons)

    View Slide

  14. History of development
    • The first release on 2018/12
    • We continue to develop with Agile
    Domain modeling
    (2~3 persons)
    Mob programming
    (4~5 persons)
    その後もアジャイルで開発を継続しています
    2018/05 2018/08 2018/10
    Development continues…
    (4~8 persons)

    View Slide

  15. Domain Driven Design
    DDDの構成要素です
    https://github.com/ketan-gote/ddd-example

    View Slide

  16. Context map
    まずはコンテキストマップについてお話します

    View Slide

  17. Context map
    progadが主となるBCで、コアドメインでもあります
    programmatic ad
    buying
    (same as core domain)

    View Slide

  18. Context map
    入札ロジックは独立したBCが責任を持ちます
    bidding logic (pricing)

    View Slide

  19. Context map
    入札ロジックを別チームであるGlazeのパートナーがカスタマイ
    ズできるようになっています
    Partner can customize
    bidding logic

    View Slide

  20. Context map
    progadとbid agentが共有するドメインオブジェクトをkernelとして
    います
    the kernel shared by
    other contexts

    View Slide

  21. Bounded context
    BCの設計にはコンウェイの法則、逆コンウェイ戦略を意識しま
    した
    • Consciously aligned with team boundaries
    • Conway’s Law & Inverse Conway Maneuver
    • Bounded context ≒ Microservice boundary
    • If you are wondering if you should split BC, I think it is better to proceed without
    splitting
    Make one team responsible for one BC

    View Slide

  22. Domain Driven Design
    次はエンティティと値オブジェクトについてです
    https://github.com/ketan-gote/ddd-example

    View Slide

  23. 11 Entities
    ドメインモデルには11のエンティティがあります
    AppList Ad Conversion LineItem Exchange AudienceIdentifierList
    BudgetAccount Advertiser InsertionOrder CompleteCandidate Campaign

    View Slide

  24. 363 Value objects
    値オブジェクトは363あります
    NoBidReasonCode VideoCreativeFile DeliveryEvent ContentTaxonomy AdCompanions BidAgentConfig WinNotice AggregateSpending Extension ApiFramework Sketch ClickDeliveryTracking
    Content Endpoint EntityLikeDescriptorSet OperatingSystem Tracker Markup AcceptanceRule Currency Purpose Adjust FeatureEngineer Feature UnboundAssets VideoFileCreative
    DeliveryUnboundEndpoints DistributionChannel AggregationGroup CostPerView FilterResult BidResponseTranslator DisplayAdPosition WinDeliveryTracking FeatureIndexer
    AttributionId VastExtension Dimension VideoCandidateFilter PartialCandidate Descriptor BitSetDescriptorSet Tracking DeliveryBoundEndpoints ViewMeasurementTracking BlankProto
    FeatureSource package HtmlMarkup IosAdvertisingIdentifier AuctionNotice ConversionRate CandidateHeader Cpm ViewableImpressionTrackingType Imp BidAgentConfigured BaseMetric
    AdType ListOwner VideoAdMeasurementContext CreativesDigest Candidate MeasurementUrls AudioCreative BidContext CompanionType Geo DisplayViewableDefinition Publisher PageUrl
    Metric Source Tune VideoAdFilter NegativeTargeting AcceptedActivity ListIntent ImageCreativeFile MeasurementEventProto AdtdpComCookieIdentifier DeniedActivity
    InventoryDescriptors ActivitySet CandidateRejection WebTargeting TargetingCategory ClickThroughRate MobileAppId ClickMeasurementTracking OperatingSystemDescriptor Targeting
    CPVBidAgentFeature MeasurementTracking TrackerBoundEndpoint LinearityMode Restrictions InappropriateContentDescriptor BlankPayload CandidateFilter AudioPlacement Frequency
    AudienceIdentifierListMeta Ipv4 RejectableCandidate Fox IosAppId CostPerAcquisition AndThenAdFilter NoBidReasonClass StandardOrderPath UserRewardedContentDescriptor
    CustomerAction SystemListOwner CustomerActionProto TimingProfile DescribableAsEngine MraidInfo AndroidAdvertisingIdentifier CostRate AndroidAppId DimensionUnit ClickTracker
    Format DisplayCreative RejectedAd EventType Ip MeasurementEvent BidRequest NamedCustomerAction HourFeature MeasurementState RealtimeBiddingProto Origin Candidates
    DeviceCategory Credit Site AdPreviewEndpoint AtomicFeature AttachmentPoint InventorySource VideoContainer TrackingSignal AdFilter KeyPerformanceIndicator AppsFlyer
    CandidateEvaluation MobilePlatformSpecificIdentifier EnvironmentTargeting CookieIdentifier BuiltinMeasurement DelayedFeedback Queryable DeliveryEventEncoder
    UserRewardedContentTargeting DisplayCreativeSubtype SeparatedCandidates MoneyExchange Ip DeliveryContext Viewability NoBidReasonCategory DeliveryState EnvironmentDescriptor
    MeasurementTool InteractionOfFeatures RichCreative ImpressionDeliveryTracking AdTypeDescriptor OrderPath VideoCreative package OpenRTBContentTaxonomy Bitrate OrderStatus User
    DescriptorSet ImageFormat package Budget ConnectionType AdvertiserListOwner Currency CookieDomain ImageCreative CreativeType Publisher Banner MarkupService BidPayload
    MeasurementEventEncoder ChargeModel MetricUnit UserAgentParser AudienceStats Ad AggregatePeriod SensitiveCategoryTargeting Audience GlazeTestBidRequestTranslator RtbShipState
    SpendingCapTarget VideoPayload Attribute StandardBidRequest Bid WinPayload FixedPricingBidAgentConfig TrackingContext AppTargeting CapStatus StandardBidAgentConfig
    DeviceProto ValueLikeDescriptorSet CardinalityDefinable AudioVideoCreativeSubtype DeliveryTracking CPVBidAgentConfig FeatureValueReader VideoFile MopubExtension
    ImpressionTracker ImpressionUnitPrice Indexable Environment ClickProto TargetingCompound VerifiedActivity CommonnessResolvable LastClickAttributionModel
    StandardBidAgentFeature AppListEntry DomainDescriptor Purpose MobilePlatformTargeting Video Money CustomerActionPayload Translatable BidDeliveryTracking ExchangeRepository
    AbstractEntityBasedTargeting Clause AppListMeta ViewabilityDescriptor StartDelayMode WinProto TradingLocation AudienceProto BidRequest AdSelector WinTracker VideoFiles
    CallToAction AppDescriptor VideoPlacement ExchangeProtocol AggregatedFrequency TemplateUrl AndThenCandidateFilter VideoProto Data BidRequestTranslator CreativeMeta
    ConversionEvent MopubProtocol Identifier CapTarget ViewThroughRate MopubVastExtension MopubBidRequestTranslator LoseNotice AuditStatus AdsDigest AttributionModel Money
    BinaryClassifier GoogleAdManagerProtocol ViewableDefinition CandidateContext FeatureIndexerTransformer ConversionClassifier Features NoExtension Producer
    VideoViewableDefinition SeparatedAds App StandardBidResponse InappropriateContentCategory Vast3Markup MeasurementUrl SeatBid Regs VideoCreativesDigest Activity GUIDPool
    Playhead Creative DurationRange TrackerUnboundEndpoint StandardContentTaxonomy AudienceDescriptor Cpm CoproductUrl BoundAssets Mraid2VastMarkup Audience
    MopubBidResponseTranslator RedirectToLandingPage VastTrackingEvent SpecReadable CreativeAttribute LogisticRegression ClickPayload GlazeTestBidResponseTranslator MediaType Geo
    BudgetAccountStatus VideoTrackerBoundEndpoint Device GoogleWinningPriceCodec EndpointResolver Ipv6 InvalidTrafficClassification CreativeStatus
    CustomerActionMeasurementTracking ExchangeDescriptor AudienceTargeting BidResponse ParsedUserAgent NoBidReason DeviceType DeliveryEventProto Ipv6 AdFormat
    SimpleTrackerBoundEndpoint VideoDeliveryTracking ContentCategoryDescriptor EventForAggregate AttributeValue CampaignPurpose Segment Policy VideoTracker ListOwnerType BidAgent
    AudienceIdentifierListEntry BidProto Placement package BidResponse Ipv4 WinningPriceCodec Device TrackingUrl LastTouchAttributionModel FactorizationMachine FrequencyCap
    AbstractValueBasedTargeting VideoProfile AssetPath DisplayPlacement package VideoCreativeMeta ExtensionTrackingType Spent AdPosition MoneyProto NaiveDescriptorSet LandingPage
    Attribution MarkupRequest Audit Item CustomerActionActivation DerivedMetric

    View Slide

  25. Entity & Value object design
    • At first I tended to make Entity
    • Anyway, let's create a Value Object !
    • Do not use primitive types for members
    最初はエンティティを作りがちですが、可能な限り値オブジェク
    トに出来ないか考えましょう

    View Slide

  26. Architecture

    View Slide

  27. Architectures
    アーキテクチャには色々な選択肢がありました

    View Slide

  28. Explicit architecture
    特にこれといったものは採用せず、Explicit Architecture という
    記事を参考に設計を進めました
    https://herbertograca.com/tag/explicit-architecture/

    View Slide

  29. Most important thing is...
    ただ一つ、「ドメインを技術的詳細から隔離する」ことが重要で

    Isolate the domain from technical
    details

    View Slide

  30. ...but, our domain is Programmatic
    Ad Buying, some technical details
    placed to domain.
    でも私たちのドメインはネット広告なので、一般的には技術的
    詳細に分類させるものもドメインに置いています

    View Slide

  31. Some technical details?
    • Connection protocol with other Exchange
    • Measurement URL by pixel
    例えばExchangeとの接続プロトコルや計測URLをドメインに置い
    ています

    View Slide

  32. 27 SBT projects
    まずはSBTのプロジェクト構成です

    View Slide

  33. Simplified project structure
    簡略化するとこのようになります

    View Slide

  34. Project dependency
    • Represent one-way dependency of layer by project division
    • Force not to write wrong dependency from outside to inside layer
    レイヤーの外から内方向への間違った依存を書けないように
    プロジェクト分割して強制しました

    View Slide

  35. Foundation
    すべてのファンデーションプロジェクト
    •So-called common library
    •Axis different from layer division
    •Stores the base code of domain,
    application and infrastructure
    layer
    •Very small (about 1 to 10 classes)

    View Slide

  36. Domain / Application layer
    すべてのドメインとアプリケーションプロジェクト
    •Directory is divided by Bounded
    Context
    •I think I should separate
    repositories by BC correctly
    •Spark applications are split
    because they have different
    paradigms
    •The project is not divided into
    applications because it is still
    small

    View Slide

  37. Application layer
    • Separate packages by functional module
    • Classify for each use case
    アプリケーションは機能モジュールごとのパッケージ、ユース
    ケースごとのクラスになっています

    View Slide

  38. Use case class
    • Dependency injection by
    constructor injection at
    infrastructure layer
    • Nothing specials
    ユースケースクラスはコンストラクタDIにより構成されています
    trait HandleClickUseCase {
    def apply(exchangeIdStr: String, uri: String): Future[Url]
    }
    class HandleClickUseCaseImpl(
    trackerEndpointResolver: TrackerEndpointResolver.CustomResponse,
    handleEndpointSignalUseCase: HandleEndpointSignalUseCase,
    adRepository: AdRepository,
    transactor: Resource[IO, Transactor[IO]]
    ) extends HandleClickUseCase
    with StrictLogging {
    }

    View Slide

  39. Adapter (Infrastructure layer)
    すべてのアダプタプロジェクト

    View Slide

  40. Adapter example
    Trait is placed in application layer
    package glaze.progad.application.budget.repository
    trait SpendingRepository {
    def append(budgetAccountId: BudgetAccountId, events: Seq[Spent]): Future[Unit]
    def sum(budgetAccountId: BudgetAccountId, period: AggregatePeriod): Future[Seq[AggregateSpending]]
    }
    package glaze.progad.adapter.repository.dynamodb.spending
    class DynamodbSpendingRepository(
    client: AmazonDynamoDBAsync @@ DynamodbSpendingRepository,
    tableName: String @@ DynamodbSpendingRepository,
    scanamoEc: ExecutionContext @@ DynamodbSpendingRepository
    ) extends SpendingRepository {
    override def append(budgetAccountId: BudgetAccountId, events: Seq[Spent]): Future[Unit] = ???
    }
    リポジトリの trait はアプリケーション層に配置しています
    Implement it as concrete class with constructor dependency injection

    View Slide

  41. Repository trait in application layer
    • Can force the domain to be pure (no Future, no IO)
    • In the DDD example, repository traits are often placed in the domain layer
    それによりドメイン層が純粋であることを強制することができま

    View Slide

  42. Driver (Infrastructure layer)
    すべてのドライバプロジェクト
    • The dependency on the HTTP
    framework is only at this layer
    • DI works in this layer
    • Other projects besides Scala
    are included (frontend,
    Lambda function)

    View Slide

  43. Domain Layer

    View Slide

  44. The domain layer is a chunk of
    domain-specific work, so I will talk
    about parts that can be used
    generically.
    ドメイン層はドメイン固有業務の塊なので、汎用的に使える部
    分をお話します

    View Slide

  45. Codec type class
    package glaze.foundation.core
    import com.twitter.util.{Decoder, Encoder}
    trait Codec[T, S] extends Encoder[T, S] with Decoder[Option[T], S]
    object Codec {
    def encode[T, S](t: T)(implicit enc: Codec[T, S]): S = enc.encode(t)
    def decode[T, S](s: S)(implicit dec: Codec[T, S]): Option[T] = dec.decode(s)
    def apply[T, S]()(implicit codec: Codec[T, S]): Codec[T, S] = codec
    def apply[T, S](encoder: T => S)(decoder: S => Option[T]): Codec[T, S] = new Codec[T, S] {
    override def encode(t: T): S = encoder(t)
    override def decode(s: S): Option[T] = decoder(s)
    }
    }
    Codecというシリアライズをサポートする型クラスです

    View Slide

  46. Example of Codec
    package glaze.kernel.domain
    import enumeratum.values.{StringEnum, StringEnumEntry}
    import glaze.foundation.core.Codec
    sealed abstract class AdType(val value: String) extends StringEnumEntry
    object AdType extends StringEnum[AdType] {
    val values: immutable.IndexedSeq[AdType] = findValues
    object Display extends AdType("Display")
    object Video extends AdType("Video")
    object Audio extends AdType("Audio")
    implicit val codecForAdTypeString: Codec[AdType, String] =
    Codec((_: AdType).value)(s => AdType.withValueOpt(s))
    }
    型クラスインスタンスはCodecはこのように実装します

    View Slide

  47. Usage of Codec
    Codec.encode[AdType, String](AdType.Video) → “Video”
    Codec.decode[AdType, String]("Video") → Some(AdType.Video)
    Codec.decode[AdType, String]("Invalid") → None
    型クラスはこのように利用します

    View Slide

  48. Codec to Doobie adapter
    package glaze.foundation.application.doobie
    import doobie.util.Meta
    import glaze.foundation.core.Codec
    trait Implicits {
    implicit def codecToDoobieMeta[T, S](implicit codec: Codec[T, S], ev: Meta[S]): Meta[T] =
    Meta[S].imap(r => codec.decode(r).getOrElse(throw new IllegalArgumentException(s"invalid format:
    $r")))(codec.encode)
    }
    Codecを用意するとこの定義によりDoobieのMeta型クラスへ暗
    黙変換されます
    If you prepare Codec, this definition will implicitly convert to Doobie's Meta type
    class

    View Slide

  49. Codec to Scanamo adapter
    package glaze.foundation.aws.dynamodb
    import glaze.foundation.core.Codec
    import org.scanamo.DynamoFormat
    import org.scanamo.error.TypeCoercionError
    trait Implicits {
    implicit def codecToDynamoFormat[T, S](implicit codec: Codec[T, S], f: DynamoFormat[S]): DynamoFormat[T] =
    DynamoFormat.xmap[T, S] { r =>
    codec.decode(r).toRight(TypeCoercionError(new IllegalArgumentException(s"invalid format: $r")))
    } {
    codec.encode
    }
    }
    ScanamoのDynamoFormat型クラスへの変換も同時に利用可
    能になります
    Conversion to Scanamo's DynamoFormat type class will also be available at the
    same time

    View Slide

  50. Should be Codec in domain layer?
    • I wondered if this transformation would be good for the domain layer
    この変換をドメイン層に置いて良いものか迷いました

    View Slide

  51. Should be Codec in domain layer?
    • I decided to put in the domain layer only about things that can be converted to
    primitives
    • Codec is not defined for an entity and a value object consisting of multiple value
    objects
    • It is expected that primitive representations will not change even if complex
    representations such as JSON are shaken in the future
    プリミティブと変換できるものについてのみドメイン層に置くこと
    にしました

    View Slide

  52. Should Codec be in domain layer?
    • Reduced conversion code between layers, so development was speeding up
    • There is no need to write separate conversions with Doobie (RDB) and Scanamo
    (KVS)
    これにより大きなデメリットもなく開発スピードが向上したと思い
    ます

    View Slide

  53. Domain event
    Domain events are defined in the domain layer by Protocol Buffers
    もう一つ、ドメインイベントの例です

    View Slide

  54. Why is .proto in domain layer?
    • At first, .proto was placed in the application layer, and in the domain layer the
    corresponding case class was written
    • Reduced development speed with conversion code hell
    • Moved .proto to the domain layer and used ScalaPB generated case class as
    domain event
    変換コードのメンテナンスが非常に辛かったのでドメイン層に
    置くことにしました

    View Slide

  55. Why .proto in domain layer?
    • Code generation from schemas such as Protocol Buffers and Avro is not fit with
    DDD
    • The advantage of code autogeneration disappears when isolated from domain
    • There are also cases where the generated code is used in the domain layer
    • I think that the problem is unlikely to occur because Protocol Buffers is schema
    compatible at the time of modification
    割り切って自動生成コードをドメイン層で利用するのもありだと
    思います

    View Slide

  56. Summary

    View Slide

  57. Thank you
    ありがとうございました
    Special thanks to @bakenezumi, @fukuo33

    View Slide