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

まずはドメインに向き合って、それからCQRSで実装する

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for memetics10 memetics10
January 10, 2026
2.6k

 まずはドメインに向き合って、それからCQRSで実装する

Avatar for memetics10

memetics10

January 10, 2026
Tweet

Transcript

  1. $234͸ΞʔΩςΫνϟύλʔϯͷҰछ͕ͩɺ ϓϩμΫτσβΠϯͷώϯτ͕຾͍ͬͯΔ ɾ.7$ ɾϨΠϠʔυΞʔΩςΫνϟ ɾΠϕϯτιʔγϯά ɾFUDʜ ɾϢʔβʔ΍υϝΠϯͷ෼ੳ ɾ֓೦Ϟσϧ ɾ69ઃܭ ɾFUDʜ

    ΞʔΩςΫνϟύλʔϯ ϓϩμΫτσβΠϯ #VU 8IZ$234 ιϑτ΢ΣΞͷ಺෦Λମܥతʹ੔ཧ͢Δํ๏ ϢʔβʔχʔζʹରԠ͢Δ੡඼Λ࡞੒͢Δϓϩηε
  2. ٕज़తʹ͸ͲͪΒ΋୯७ͳߏ଄ମͱ͍͏఺Ͱಉ͡Ͱɺ໋໊ํ๏ʹҧ͍͕͋Δ ݴޠతͳҙຯʹ஫໨͠ɺ υϝΠϯϞσϧͱϢϏΩλεݴޠͷ୳ࡧʹ࢖͏ type DeactivateInventoryItem struct { InventoryItemID string Comment

    string } type InventoryItemDeactivated struct { InventoryItemID string Comment string } &%"΋ɺΠϕϯτʹ࣍ͷࢦؚ͕ࣔ·Εͳ͍ҙຯతͳཧ༝Ͱૄ݁߹ʹͳΔ $PNNBOE&WFOU
  3. ΠϕϯτετʔϛϯάΛɺσβΠϯͷख๏ͱͯ͠׆༻͢Δ ҟͳΔόοΫάϥ΢ϯυΛ࣋ͭਓͷߟ͑Λൃࢄͤ͞ɺΧϥʔύζϧͱର࿩Λ௨ͯ͠߹ҙͤ͞Δ ·ͣ͸໰୊Λ޿͘୳ΓɺͦΕ͔Βղܾ΁ऩଋ͍ͤͯ͘͞఺ͰɺσβΠϯࢥߟʹΑ͘ࣅ͍ͯΔ Πϕϯτ ϙϦγʔ ू໿ ίϚϯυ ίϚϯυ Πϕϯτ ू໿

    %PO/PSNBO 5IF%FTJHOPG&WFSZEBZ5IJOHT ൴Β͸ɺຊ౰ͷ໰୊͕Կ͔ΛݟఆΊΔ·Ͱ͸ղܾΛ୳ͦ͏ͱ͠ͳ͍ɻ ໰୊͕ఆ·ͬͨͱ͖Ͱ͑͞ɺ໰୊Λղܾ͠Α͏ͱ͸͠ͳ͍ɻ ൴Β͸ཱͪࢭ·ͬͯ޿͍ൣғ͔ΒՄೳͳ౴͑Λ୳͢ɻͦ͏ͯ͠ॳΊͯɺ࠷ऴతʹ൴ΒͷఏҊΛऩଋ͍ͤͯ͘͞ɻ ͜ͷϓϩηε͸σβΠϯࢥߟͱݺ͹Ε͍ͯΔɻ &WFOU4UPSNJOH
  4. ஫จ͞Εͨ ஫จ͞Εͨ Βඞͣ ஫จ ஫จ͢Δ υϦϯΫΛ ࡞Δ υϦϯΫ υϦϯΫ͕ ࡞ΒΕͨ

    υϦϯΫ͕ ׬੒ͨ͠Β ඞͣ ٬ͷ໊લΛ ݺͿ ࢧ෷͍Λ͢ Δ ࢧ෷͍ ࢧ෷͍͕׬ ྃͨ͠ ࢧ෷͍Ͱ͖ ͳ͔ͬͨ৔ ߹͸ υϦϯΫΛ ࣺͯΔ ஫จ͢Δ ࢧ෷͍υ ϦϯΫ࡞੒ ࢧ෷͍υ ϦϯΫ࡞੒ ׬ྃ "HHSFHBUF1PMJDZ ੍໿ΛकΓͳ͕ΒɺॊೈͰϊϯϒϩοΩϯάͳΠϯλϥΫγϣϯΛߟ͑Δ (SFHPS)PIQF 4UBSCVDLT%PFT/PU6TF5XP1IBTF$PNNJU XFDBOTFFUIBUUIFSFBMXPSMEJTPGUFOBTZODISPOPVT 0VSEBJMZMJWFTDPOTJTUTPGNBOZDPPSEJOBUFE CVUBTZODISPOPVTJOUFSBDUJPOT %#ϩοΫ͢Ε͹͍͍ɺͱٕज़ओಋͰߟ͑ͳ͍Α͏ʹ͢Δ
  5. ঎඼ͷ஫จ͕ ׬ྃͨ͠ ஫จ ঎඼ͷ஫จΛ ׬ྃͤ͞Δ αϒεΫϦϓ γϣϯొ࿥͢ Δ αϒεΫ঎඼ ͷ৔߹

    αϒεΫϦϓ γϣϯొ࿥͞ Εͨ αϒεΫϦϓ γϣϯ ౤ߘ͕ ެ։͞Εͨ ౤ߘ ౤ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ؅ཧऀ ސ٬༻ͷ ౤ߘϏϡʔ 'JSTU.PEFM
  6. ঎඼ͷ஫จ͕ ׬ྃͨ͠ αϒεΫ঎඼ ͷ৔߹ ஫จ ঎඼ͷ஫จΛ ׬ྃͤ͞Δ αϒεΫϦ ϓγϣϯొ ࿥͢Δ

    αϒεΫϦϓ γϣϯొ࿥͞ Εͨ αϒεΫϦϓ γϣϯ ౤ߘ͕ ެ։͞Εͨ ౤ߘ ౤ߘΛ ެ։͢Δ ސ٬ ίϛϡχςΟ ؅ཧऀ ސ٬༻ͷ ౤ߘϏϡʔ αϒεΫϦϓ γϣϯొ࿥͞ ΕͨΒ ձһূΛड ͚औΔ ձһূ͕ड͚ औΒΕͨ ձһূ ձһূ ձһূ͕ఀࢭ ͞Εͨ ձһূΛఀ ࢭ͢Δ αϒεΫϦ ϓγϣϯΛ ແޮԽ͢Δ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞Εͨ αϒεΫϦϓ γϣϯ αϒεΫϦϓ γϣϯ͕ແޮ Խ͞ΕͨΒ ސ٬ αʔϏε΁ͷࢧ෷͍ͱར༻ͷݖརΛ෼཭͠ɺ ձһূΛड͚औͬͨɺͱ͍͏ࣄ࣮Λ௥Ճ 3FDPOTJEFS%PNBJO.PEFM
  7. Collaborative Domain Independent Domain 1VCMJD%BUB ঎඼Ձ֨ 1SJWBUF%BUB ঎඼໊ ঎඼આ໌ ঎඼ը૾

    CRUD, Stereotype Architecture CQRS+ES Publish Data 6EJ%BIBO IUUQTYDPN6EJ%BIBOTUBUVT 1VCMJDEBUBXPVMEOPUCFIBOEMFEJOUIFTBNFWFSUJDBMTMJDFBTUIFQSJWBUFEBUB $234JTBNPSFBQQSPQSJBUFDIPJDFGPSQVCMJDEBUB BOEUIFSF  JUDBONBLFTFOTFUPIBWFEJ ff FSFOUWFSUJDBMTMJDFTGPSUIFDPNNBOEBOERVFSZQBSUT #PVOEFE$POUFYU 5PQ-FWFM"SDIJUFDUVSF
  8. package subscription // ू໿ϧʔτ type Subscription struct { ID uuid.UUID

    Status Status } // ू໿ϧʔτͷίϚϯυϝιου͕ɺෆม৚݅Λҡ࣋͠ͳ͕ΒΠϕϯτΛ࡞੒͢Δ func (s Subscription) Deactivate(deactivateAt time.Time) (Deactivated, error) { // write some invariants here if s.Status == Deactivated { return Deactivated{}, fmt.Errorf("subscription is already deactivated: %w", errors.ErrPreconditionFailed) } return Deactivated{SubscriptionID: s.ID, DeactivateAt: deactivateAt}, nil } // Πϕϯτ type Deactivated struct { SubscriptionID uuid.UUID DeactivatedAt time.Time } func (SubscriptionDeactivated) IsSubscriptionEvent() {} type Event interface { IsSubscriptionEvent() } // ϦϙδτϦ: Πϕϯτͷอଘͱू໿ͷϩʔυ͕Մೳ type Repository interface { Store(context.Context, Event) error Load(context.Context, uuid.UUID) (Subscription, error) } *NQMFNFOUJOH"HHSFHBUF
  9. package command // ίϚϯυ: ૢ࡞ͷ໊લͱͦΕ࣮ߦ͢ΔͨΊʹඞཁͳೖྗσʔλΛ࣋ͭ type DeactivateSubscriptionCommand struct { SubscriptionID

    uuid.UUID } // ίϚϯυϓϩηοαʔ: ू໿ʹίϚϯυॲཧΛҕৡ͢Δ func (p DeactivateSubscriptionCommandProcessor) Exec(ctx context.Context, command DeactivateSubscriptionCommand) (subscription.Deactivated, error) { aSubscription, err := p.SubscriptionRepo.Load(ctx, command.SubscriptionID) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Load: %w", err) } event, err := aSubscription.Deactivate(time.Now()) if err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Subscription.Deactivate: %w", err) } if err := p.SubscriptionRepo.Store(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("subscription.Repository.Store: %w", err) } if err := p.EventPublisher.Publish(ctx, event); err != nil { return subscription.Deactivated{}, fmt.Errorf("EventPublisher.Publish: %w", err) } return event, nil } *NQMFNFOUJOH$PNNBOE
  10. package redis type SubscriptionDeactivatedEventHandler struct { AsynqClient *asynq.Client } func

    (h SubscriptionDeactivatedEventHandler) HandleSubscriptionDeactivated(ctx context.Context, event subscription.Deactivated) error { // Ϗδωεϧʔϧ͸੾Γग़͓ͯ͘͠ if policy.CanDeactivateMembershipcard(event) { payload, err := json.Marshal(task.DeactivateMembershipcardPayload{SubscriptionID: event.SubscriptionID}) if err != nil { return fmt.Errorf("marshal payload: %w", err) } // Ωϡʔʹ٧ΊͯผϓϩηεʹॲཧΛ೚ͤΔ _, err = h.AsynqClient.EnqueueContext(ctx, asynq.NewTask(task.DeactivateMembershipcard, payload)) if err != nil { return fmt.Errorf("enqueue process deactivate membershipcard task: %w", err) } } return nil } ˞࣮ߦͰ͖ͳ͍৔߹ͷϦτϥΠՄೳੑɺෳ਺ճ࣮ߦ͞Εͨ৔߹ͷႈ౳ੑɺଞͷΠϕϯτͱͷॱংੑɺΛߟྀ͓ͯ͘͠ඞཁ͋Γ *NQMFNFOUJOH1PMJDZ
  11. package query // ΫΤϦαʔϏε type SubscriptionService interface { QueryPage(ctx context.Context,

    filter SubscriptionFilter, sorter SubscriptionSorter, limit int, cursor *string) (Page[Subscription], error) QueryByIDs(context.Context, []string) ([]Subscription, error) QueryByID(context.Context, string) (Subscription, error) } // ΫΤϦϞσϧ type Subscription struct { ID string CustomerID string ProductID string NextRenewAt time.Time LastRenewedAt time.Time } // ϑΟϧλʔ&ιʔτ type SubscriptionFilter struct { CustomerID string } type SubscriptionSorter struct { Key SubscriptionSortKey Direction SortDirection } 2VFSZ4FSWJDF.PEFM